Compare commits

...

23 Commits

Author SHA1 Message Date
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
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
4a22861860 Sort the files after fetching 2022-06-02 14:04:52 +03:00
muzamil
b09d029931 Merge pull request #196 from XRPLF/feat/fee-hint
Fee hints in transactions.
2022-06-01 16:21:24 +05:30
muzam1l
b2dc49754f Error toast in suggest button! 2022-06-01 15:51:08 +05:30
muzam1l
6f636645f7 Fix json saving mismatch 2022-05-31 00:53:58 +05:30
muzam1l
377c963c7a FIx json mode schema 2022-05-30 23:32:53 +05:30
muzam1l
ae038f17ff Suggest fee button in transaction ui 2022-05-30 23:01:46 +05:30
Vaclav Barta
0d8f2c31e7 Feature/hook documentation (#197)
* updated hooks-entry-points-check

* added hooks-trivial-cbak

* updated hooks-hash-buf-len: nonce => etxn_nonce + ledger_nonce
2022-05-30 12:49:19 +02:00
muzam1l
da9986eb66 Remove unnecessary console logs 2022-05-27 22:46:15 +05:30
muzam1l
a21350770e Fee hints in transactions. 2022-05-27 16:44:01 +05:30
Valtteri Karesto
49dfd43220 Merge pull request #193 from XRPLF/feat/fee-estimates
Feat/fee estimates
2022-05-27 12:23:50 +03:00
Valtteri Karesto
4472957f5c Updated based on feedback 2022-05-27 10:43:16 +03:00
muzamil
ca46707bb5 Merge pull request #195 from XRPLF/feat/tx_hash_link
Show tx hash instead of server ledger index in deploy log.
2022-05-26 15:28:31 +05:30
Valtteri Karesto
9a6ef2c393 Minor fixes based on feedback 2022-05-25 13:39:52 +03:00
Valtteri Karesto
56203ce9c6 Remove package-lock 2022-05-25 13:10:05 +03:00
Valtteri Karesto
933bdb5968 Add fee estimate button to fee and update the deploying 2022-05-25 13:05:04 +03:00
Valtteri Karesto
864711697b Update accounts 2022-05-25 13:03:49 +03:00
Valtteri Karesto
e5eaf09721 Just return the fee values, no mutating 2022-05-25 13:03:38 +03:00
16 changed files with 586 additions and 12363 deletions

View File

@@ -469,7 +469,7 @@ const Accounts: FC<AccountProps> = (props) => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
<SetHookDialog account={account} /> <SetHookDialog accountAddress={account.address} />
</div> </div>
)} )}
</Flex> </Flex>

View File

@@ -21,11 +21,11 @@ import {
import { TTS, tts } from "../utils/hookOnCalculator"; import { TTS, tts } from "../utils/hookOnCalculator";
import { deployHook } from "../state/actions"; import { deployHook } from "../state/actions";
import type { IAccount } from "../state";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state from "../state"; import state from "../state";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { sha256 } from "../state/actions/deployHook"; 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, label: key,
@@ -37,6 +37,7 @@ export type SetHookData = {
value: keyof TTS; value: keyof TTS;
label: string; label: string;
}[]; }[];
Fee: string;
HookNamespace: string; HookNamespace: string;
HookParameters: { HookParameters: {
HookParameter: { HookParameter: {
@@ -52,8 +53,11 @@ export type SetHookData = {
// }[]; // }[];
}; };
export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => { export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
({ accountAddress }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const account = snap.accounts.find((acc) => acc.address === accountAddress);
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false); const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const { const {
register, register,
@@ -61,16 +65,21 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
control, control,
watch, watch,
setValue, setValue,
getValues,
formState: { errors }, formState: { errors },
} = useForm<SetHookData>({ } = useForm<SetHookData>({
defaultValues: { defaultValues: {
HookNamespace: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "", HookNamespace:
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
Invoke: transactionOptions.filter((to) => to.label === "ttPAYMENT"),
}, },
}); });
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control, control,
name: "HookParameters", // unique name for your Field Array name: "HookParameters", // unique name for your Field Array
}); });
const [formInitialized, setFormInitialized] = useState(false);
const [estimateLoading, setEstimateLoading] = useState(false);
// Update value if activeWat changes // Update value if activeWat changes
useEffect(() => { useEffect(() => {
@@ -78,6 +87,7 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
"HookNamespace", "HookNamespace",
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "" snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
); );
setFormInitialized(true);
}, [snap.activeWat, snap.files, setValue]); }, [snap.activeWat, snap.files, setValue]);
// const { // const {
// fields: grantFields, // fields: grantFields,
@@ -100,6 +110,24 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
calculateHashedValue(); calculateHashedValue();
}, [namespace, calculateHashedValue]); }, [namespace, calculateHashedValue]);
// Calcucate 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", res.base_fee);
}
})();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formInitialized]);
if (!account) { if (!account) {
return null; return null;
} }
@@ -118,7 +146,6 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
} }
toast.error(`Transaction failed! (${res?.engine_result_message})`); toast.error(`Transaction failed! (${res?.engine_result_message})`);
}; };
return ( return (
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}> <Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -178,6 +205,7 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
<Input readOnly value={hashedNamespace} /> <Input readOnly value={hashedNamespace} />
</Box> </Box>
</Box> </Box>
<Box css={{ width: "100%" }}> <Box css={{ width: "100%" }}>
<Label style={{ marginBottom: "10px", display: "block" }}> <Label style={{ marginBottom: "10px", display: "block" }}>
Hook parameters Hook parameters
@@ -221,6 +249,69 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
</Button> </Button>
</Stack> </Stack>
</Box> </Box>
<Box css={{ width: "100%", position: "relative" }}>
<Label>Fee</Label>
<Box css={{ display: "flex", alignItems: "center" }}>
<Input
type="number"
{...register("Fee", { required: true })}
autoComplete={"off"}
defaultValue={10000}
css={{
"-moz-appearance": "textfield",
"&::-webkit-outer-spin-button": {
"-webkit-appearance": "none",
margin: 0,
},
"&::-webkit-inner-spin-button ": {
"-webkit-appearance": "none",
margin: 0,
},
}}
/>
<Button
size="xs"
variant="primary"
outline
isLoading={estimateLoading}
css={{
position: "absolute",
right: "$2",
fontSize: "$xs",
cursor: "pointer",
alignContent: "center",
display: "flex",
}}
onClick={async (e) => {
e.preventDefault();
setEstimateLoading(true);
const formValues = getValues();
try {
const tx = await prepareDeployHookTx(
account,
formValues
);
if (tx) {
const res = await estimateFee(tx, account);
if (res && res.base_fee) {
setValue("Fee", res.base_fee);
}
}
} catch (err) {}
setEstimateLoading(false);
}}
>
Suggest
</Button>
</Box>
{errors.Fee?.type === "required" && (
<Box css={{ display: "inline", color: "$red11" }}>
Fee is required
</Box>
)}
</Box>
{/* <Box css={{ width: "100%" }}> {/* <Box css={{ width: "100%" }}>
<label style={{ marginBottom: "10px", display: "block" }}> <label style={{ marginBottom: "10px", display: "block" }}>
Hook Grants Hook Grants
@@ -301,6 +392,9 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; }
);
SetHookDialog.displayName = "SetHookDialog";
export default SetHookDialog; export default SetHookDialog;

View File

@@ -14,6 +14,8 @@ import Button from "../Button";
import Flex from "../Flex"; import Flex from "../Flex";
import { TxJson } from "./json"; import { TxJson } from "./json";
import { TxUI } from "./ui"; import { TxUI } from "./ui";
import { default as _estimateFee } from "../../utils/estimateFee";
import toast from 'react-hot-toast';
export interface TransactionProps { export interface TransactionProps {
header: string; header: string;
@@ -76,13 +78,19 @@ const Transaction: FC<TransactionProps> = ({
} else { } else {
setState({ txIsDisabled: false }); setState({ txIsDisabled: false });
} }
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading]); }, [
selectedAccount?.value,
selectedTransaction?.value,
setState,
txIsLoading,
]);
const submitTest = useCallback(async () => { const submitTest = useCallback(async () => {
let st: TransactionState | undefined; let st: TransactionState | undefined;
const tt = txState.selectedTransaction?.value;
if (viewType === "json") { if (viewType === "json") {
// save the editor state first // save the editor state first
const pst = prepareState(editorValue || '', txState); const pst = prepareState(editorValue || "", tt);
if (!pst) return; if (!pst) return;
st = setState(pst); st = setState(pst);
@@ -102,7 +110,7 @@ const Transaction: FC<TransactionProps> = ({
const options = prepareOptions(st); const options = prepareOptions(st);
if (options.Destination === null) { if (options.Destination === null) {
throw Error("Destination account cannot be null") throw Error("Destination account cannot be null");
} }
await sendTransaction(account, options, { logPrefix }); await sendTransaction(account, options, { logPrefix });
@@ -116,7 +124,17 @@ const Transaction: FC<TransactionProps> = ({
} }
} }
setState({ txIsLoading: false }); setState({ txIsLoading: false });
}, [viewType, accounts, txIsDisabled, setState, header, editorValue, txState, selectedAccount?.value, prepareOptions]); }, [
viewType,
accounts,
txIsDisabled,
setState,
header,
editorValue,
txState,
selectedAccount?.value,
prepareOptions,
]);
const resetState = useCallback(() => { const resetState = useCallback(() => {
modifyTransaction(header, { viewType }, { replaceState: true }); modifyTransaction(header, { viewType }, { replaceState: true });
@@ -129,6 +147,31 @@ const Transaction: FC<TransactionProps> = ({
[editorSavedValue, editorSettings.tabSize, prepareOptions] [editorSavedValue, editorSettings.tabSize, prepareOptions]
); );
const estimateFee = useCallback(
async (st?: TransactionState, opts?: { silent?: boolean }) => {
const state = st || txState;
const ptx = prepareOptions(state);
const account = accounts.find(
acc => acc.address === state.selectedAccount?.value
);
if (!account) {
if (!opts?.silent) {
toast.error("Please select account from the list.")
}
return
};
ptx.Account = account.address;
ptx.Sequence = account.sequence;
const res = await _estimateFee(ptx, account, opts);
const fee = res?.base_fee;
setState({ estimatedFee: fee });
return fee;
},
[accounts, prepareOptions, setState, txState]
);
return ( return (
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}> <Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
{viewType === "json" ? ( {viewType === "json" ? (
@@ -137,9 +180,10 @@ const Transaction: FC<TransactionProps> = ({
header={header} header={header}
state={txState} state={txState}
setState={setState} setState={setState}
estimateFee={estimateFee}
/> />
) : ( ) : (
<TxUI state={txState} setState={setState} /> <TxUI state={txState} setState={setState} estimateFee={estimateFee} />
)} )}
<Flex <Flex
row row

View File

@@ -29,6 +29,7 @@ interface JsonProps {
header?: string; header?: string;
setState: (pTx?: Partial<TransactionState> | undefined) => void; setState: (pTx?: Partial<TransactionState> | undefined) => void;
state: TransactionState; state: TransactionState;
estimateFee?: () => Promise<string | undefined>;
} }
export const TxJson: FC<JsonProps> = ({ export const TxJson: FC<JsonProps> = ({
@@ -38,22 +39,37 @@ export const TxJson: FC<JsonProps> = ({
setState, setState,
}) => { }) => {
const { editorSettings, accounts } = useSnapshot(state); const { editorSettings, accounts } = useSnapshot(state);
const { editorValue = value, selectedTransaction } = txState; const { editorValue = value, estimatedFee } = txState;
const { theme } = useTheme(); const { theme } = useTheme();
const [hasUnsaved, setHasUnsaved] = useState(false); const [hasUnsaved, setHasUnsaved] = useState(false);
const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value
);
useEffect(() => { useEffect(() => {
setState({ editorValue: value }); setState({ editorValue: value });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]); }, [value]);
useEffect(() => {
const parsed = parseJSON(editorValue);
if (!parsed) return;
const tt = parsed.TransactionType;
const tx = transactionsData.find(t => t.TransactionType === tt);
if (tx) setCurrTxType(tx.TransactionType);
else {
setCurrTxType(undefined);
}
}, [editorValue]);
useEffect(() => { useEffect(() => {
if (editorValue === value) setHasUnsaved(false); if (editorValue === value) setHasUnsaved(false);
else setHasUnsaved(true); else setHasUnsaved(true);
}, [editorValue, value]); }, [editorValue, value]);
const saveState = (value: string, txState: TransactionState) => { const saveState = (value: string, transactionType?: string) => {
const tx = prepareState(value, txState); const tx = prepareState(value, transactionType);
if (tx) setState(tx); if (tx) setState(tx);
}; };
@@ -68,7 +84,7 @@ export const TxJson: FC<JsonProps> = ({
const onExit = (value: string) => { const onExit = (value: string) => {
const options = parseJSON(value); const options = parseJSON(value);
if (options) { if (options) {
saveState(value, txState); saveState(value, currTxType);
return; return;
} }
showAlert("Error!", { showAlert("Error!", {
@@ -82,9 +98,10 @@ export const TxJson: FC<JsonProps> = ({
const path = `file:///${header}`; const path = `file:///${header}`;
const monaco = useMonaco(); const monaco = useMonaco();
const getSchemas = useCallback((): any[] => { const getSchemas = useCallback(async (): Promise<any[]> => {
const tt = selectedTransaction?.value; const txObj = transactionsData.find(
const txObj = transactionsData.find(td => td.TransactionType === tt); td => td.TransactionType === currTxType
);
let genericSchemaProps: any; let genericSchemaProps: any;
if (txObj) { if (txObj) {
@@ -98,7 +115,6 @@ export const TxJson: FC<JsonProps> = ({
{} {}
); );
} }
return [ return [
{ {
uri: "file:///main-schema.json", // id of the first schema uri: "file:///main-schema.json", // id of the first schema
@@ -130,6 +146,9 @@ export const TxJson: FC<JsonProps> = ({
Amount: { Amount: {
$ref: "file:///amount-schema.json", $ref: "file:///amount-schema.json",
}, },
Fee: {
$ref: "file:///fee-schema.json",
},
}, },
}, },
}, },
@@ -141,17 +160,30 @@ export const TxJson: FC<JsonProps> = ({
enum: accounts.map(acc => acc.address), enum: accounts.map(acc => acc.address),
}, },
}, },
{
uri: "file:///fee-schema.json",
schema: {
type: "string",
title: "Fee type",
const: estimatedFee,
description: estimatedFee
? "Above mentioned value is recommended base fee"
: undefined,
},
},
{ {
...amountSchema, ...amountSchema,
}, },
]; ];
}, [accounts, header, selectedTransaction?.value]); }, [accounts, currTxType, estimatedFee, header]);
useEffect(() => { useEffect(() => {
if (!monaco) return; if (!monaco) return;
getSchemas().then(schemas => {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true, validate: true,
schemas: getSchemas(), schemas,
});
}); });
}, [getSchemas, monaco]); }, [getSchemas, monaco]);
@@ -184,19 +216,13 @@ export const TxJson: FC<JsonProps> = ({
// register onExit cb // register onExit cb
const model = editor.getModel(); const model = editor.getModel();
model?.onWillDispose(() => onExit(model.getValue())); model?.onWillDispose(() => onExit(model.getValue()));
// set json defaults
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: getSchemas(),
});
}} }}
theme={theme === "dark" ? "dark" : "light"} theme={theme === "dark" ? "dark" : "light"}
/> />
{hasUnsaved && ( {hasUnsaved && (
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}> <Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}>
This file has unsaved changes.{" "} This file has unsaved changes.{" "}
<Link onClick={() => saveState(editorValue, txState)}>save</Link>{" "} <Link onClick={() => saveState(editorValue, currTxType)}>save</Link>{" "}
<Link onClick={discardChanges}>discard</Link> <Link onClick={discardChanges}>discard</Link>
</Text> </Text>
)} )}

View File

@@ -1,4 +1,4 @@
import { FC } from "react"; import { FC, useCallback, useState } from "react";
import Container from "../Container"; import Container from "../Container";
import Flex from "../Flex"; import Flex from "../Flex";
import Input from "../Input"; import Input from "../Input";
@@ -9,17 +9,26 @@ import {
TransactionState, TransactionState,
transactionsData, transactionsData,
TxFields, TxFields,
getTxFields,
} from "../../state/transactions"; } from "../../state/transactions";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state from "../../state"; import state from "../../state";
import { streamState } from "../DebugStream"; import { streamState } from "../DebugStream";
import { Button } from "..";
interface UIProps { interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => void; setState: (
pTx?: Partial<TransactionState> | undefined
) => TransactionState | undefined;
state: TransactionState; state: TransactionState;
estimateFee?: (...arg: any) => Promise<string | undefined>;
} }
export const TxUI: FC<UIProps> = ({ state: txState, setState }) => { export const TxUI: FC<UIProps> = ({
state: txState,
setState,
estimateFee,
}) => {
const { accounts } = useSnapshot(state); const { accounts } = useSnapshot(state);
const { const {
selectedAccount, selectedAccount,
@@ -45,32 +54,54 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
})) }))
.filter(acc => acc.value !== selectedAccount?.value); .filter(acc => acc.value !== selectedAccount?.value);
const resetOptions = (tt: string) => { const [feeLoading, setFeeLoading] = useState(false);
const txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === tt const resetOptions = useCallback(
(tt: string) => {
const fields = getTxFields(tt);
if (!fields.Destination) setState({ selectedDestAccount: null });
return setState({ txFields: fields });
},
[setState]
); );
if (!txFields) return setState({ txFields: {} });
const _txFields = Object.keys(txFields)
.filter(key => !["TransactionType", "Account", "Sequence"].includes(key))
.reduce<TxFields>(
(tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf),
{}
);
if (!_txFields.Destination) setState({ selectedDestAccount: null });
setState({ txFields: _txFields });
};
const handleSetAccount = (acc: SelectOption) => { const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc }); setState({ selectedAccount: acc });
streamState.selectedAccount = acc; streamState.selectedAccount = acc;
}; };
const handleSetField = useCallback(
(field: keyof TxFields, value: string, opFields?: TxFields) => {
const fields = opFields || txFields;
const obj = fields[field];
setState({
txFields: {
...fields,
[field]: typeof obj === "object" ? { ...obj, $value: value } : value,
},
});
},
[setState, txFields]
);
const handleEstimateFee = useCallback(
async (state?: TransactionState, silent?: boolean) => {
setFeeLoading(true);
const fee = await estimateFee?.(state, { silent });
if (fee) handleSetField("Fee", fee, state?.txFields);
setFeeLoading(false);
},
[estimateFee, handleSetField]
);
const handleChangeTxType = (tt: SelectOption) => { const handleChangeTxType = (tt: SelectOption) => {
setState({ selectedTransaction: tt }); setState({ selectedTransaction: tt });
resetOptions(tt.value);
const newState = resetOptions(tt.value);
handleEstimateFee(newState, true);
}; };
const specialFields = ["TransactionType", "Account", "Destination"]; const specialFields = ["TransactionType", "Account", "Destination"];
@@ -87,7 +118,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
height: "calc(100% - 45px)", height: "calc(100% - 45px)",
}} }}
> >
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}> <Flex column fluid css={{ height: "100%", overflowY: "auto", pr: "$1" }}>
<Flex <Flex
row row
fluid fluid
@@ -174,16 +205,17 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
} }
let isXrp = typeof _value === "object" && _value.$type === "xrp"; let isXrp = typeof _value === "object" && _value.$type === "xrp";
const isFee = field === "Fee";
return ( return (
<Flex column key={field} css={{ mb: "$2", pr: "1px" }}>
<Flex <Flex
key={field}
row row
fluid fluid
css={{ css={{
justifyContent: "flex-end", justifyContent: "flex-end",
alignItems: "center", alignItems: "center",
mb: "$3", position: "relative",
pr: "1px",
}} }}
> >
<Text muted css={{ mr: "$3" }}> <Text muted css={{ mr: "$3" }}>
@@ -192,18 +224,30 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
<Input <Input
value={value} value={value}
onChange={e => { onChange={e => {
setState({ handleSetField(field, e.target.value);
txFields: {
...txFields,
[field]:
typeof _value === "object"
? { ..._value, $value: e.target.value }
: e.target.value,
},
});
}} }}
css={{ width: "70%", flex: "inherit" }} css={{ width: "70%", flex: "inherit" }}
/> />
{isFee && (
<Button
size="xs"
variant="primary"
outline
isLoading={feeLoading}
css={{
position: "absolute",
right: "$2",
fontSize: "$xs",
cursor: "pointer",
alignContent: "center",
display: "flex",
}}
onClick={() => handleEstimateFee()}
>
Suggest
</Button>
)}
</Flex>
</Flex> </Flex>
); );
})} })}

12057
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -213,7 +213,7 @@ const Home: NextPage = () => {
> >
<main style={{ display: "flex", flex: 1, position: "relative" }}> <main style={{ display: "flex", flex: 1, position: "relative" }}>
<HooksEditor /> <HooksEditor />
{snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() === {snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"c" && ( "c" && (
<Hotkeys <Hotkeys
keyName="command+b,ctrl+b" keyName="command+b,ctrl+b"

View File

@@ -50,11 +50,7 @@ function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
return result; return result;
} }
/* deployHook function turns the wasm binary into export const prepareDeployHookTx = async (
* hex string, signs the transaction and deploys it to
* Hooks testnet.
*/
export const deployHook = async (
account: IAccount & { name?: string }, account: IAccount & { name?: string },
data: SetHookData data: SetHookData
) => { ) => {
@@ -93,13 +89,12 @@ export const deployHook = async (
// } // }
// } // }
// }); // });
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const tx = { const tx = {
Account: account.address, Account: account.address,
TransactionType: "SetHook", TransactionType: "SetHook",
Sequence: account.sequence, Sequence: account.sequence,
Fee: "100000", Fee: data.Fee,
Hooks: [ Hooks: [
{ {
Hook: { Hook: {
@@ -118,15 +113,28 @@ export const deployHook = async (
}, },
], ],
}; };
return tx;
const keypair = derive.familySeed(account.secret);
try {
// Update tx Fee value with network estimation
await estimateFee(tx, keypair);
} catch (err) {
// use default value what you defined earlier
console.log(err);
} }
};
/* deployHook function turns the wasm binary into
* hex string, signs the transaction and deploys it to
* Hooks testnet.
*/
export const deployHook = async (
account: IAccount & { name?: string },
data: SetHookData
) => {
if (typeof window !== "undefined") {
const tx = await prepareDeployHookTx(account, data);
if (!tx) {
return;
}
if (!state.client) {
return;
}
const keypair = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, keypair); const { signedTransaction } = sign(tx, keypair);
const currentAccount = state.accounts.find( const currentAccount = state.accounts.find(
(acc) => acc.address === account.address (acc) => acc.address === account.address
@@ -137,7 +145,7 @@ export const deployHook = async (
let submitRes; let submitRes;
try { try {
submitRes = await state.client.send({ submitRes = await state.client?.send({
command: "submit", command: "submit",
tx_blob: signedTransaction, tx_blob: signedTransaction,
}); });
@@ -216,7 +224,8 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
const keypair = derive.familySeed(account.secret); const keypair = derive.familySeed(account.secret);
try { try {
// Update tx Fee value with network estimation // Update tx Fee value with network estimation
await estimateFee(tx, keypair); const res = await estimateFee(tx, account);
tx["Fee"] = res?.base_fee ? res?.base_fee : "1000";
} catch (err) { } catch (err) {
// use default value what you defined earlier // use default value what you defined earlier
console.log(err); console.log(err);

View File

@@ -58,6 +58,29 @@ export const fetchFiles = (gistId: string) => {
language: res.data.files?.[filename]?.language?.toLowerCase() || "", language: res.data.files?.[filename]?.language?.toLowerCase() || "",
content: res.data.files?.[filename]?.content || "", 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; state.loading = false;
if (files.length > 0) { if (files.length > 0) {
state.logs.push({ state.logs.push({

View File

@@ -20,8 +20,8 @@ export const sendTransaction = async (account: IAccount, txOptions: TransactionO
const { Fee = "1000", ...opts } = txOptions const { Fee = "1000", ...opts } = txOptions
const tx: TransactionOptions = { const tx: TransactionOptions = {
Account: account.address, Account: account.address,
Sequence: account.sequence, // TODO auto-fillable Sequence: account.sequence,
Fee, // TODO auto-fillable Fee, // TODO auto-fillable default
...opts ...opts
}; };

View File

@@ -19,7 +19,8 @@ export interface TransactionState {
txFields: TxFields; txFields: TxFields;
viewType: 'json' | 'ui', viewType: 'json' | 'ui',
editorSavedValue: null | string, editorSavedValue: null | string,
editorValue?: string editorValue?: string,
estimatedFee?: string
} }
@@ -93,7 +94,7 @@ export const modifyTransaction = (
Object.keys(partialTx).forEach(k => { Object.keys(partialTx).forEach(k => {
// Typescript mess here, but is definetly safe! // Typescript mess here, but is definetly safe!
const s = tx.state as any; const s = tx.state as any;
const p = partialTx as any; const p = partialTx as any; // ? Make copy
if (!deepEqual(s[k], p[k])) s[k] = p[k]; if (!deepEqual(s[k], p[k])) s[k] = p[k];
}); });
@@ -140,7 +141,7 @@ export const prepareTransaction = (data: any) => {
} }
// editor value to state // editor value to state
export const prepareState = (value: string, txState: TransactionState) => { export const prepareState = (value: string, transactionType?: string) => {
const options = parseJSON(value); const options = parseJSON(value);
if (!options) { if (!options) {
showAlert("Error!", { showAlert("Error!", {
@@ -151,7 +152,7 @@ export const prepareState = (value: string, txState: TransactionState) => {
const { Account, TransactionType, Destination, ...rest } = options; const { Account, TransactionType, Destination, ...rest } = options;
let tx: Partial<TransactionState> = {}; let tx: Partial<TransactionState> = {};
const { txFields } = txState const txFields = getTxFields(transactionType)
if (Account) { if (Account) {
const acc = state.accounts.find(acc => acc.address === Account); const acc = state.accounts.find(acc => acc.address === Account);
@@ -206,7 +207,7 @@ export const prepareState = (value: string, txState: TransactionState) => {
if (isXrp) { if (isXrp) {
rest[field] = { rest[field] = {
$type: "xrp", $type: "xrp",
$value: +value / 1000000, // TODO maybe use bigint? $value: +value / 1000000, // ! maybe use bigint?
}; };
} else if (typeof value === "object") { } else if (typeof value === "object") {
rest[field] = { rest[field] = {
@@ -222,4 +223,24 @@ export const prepareState = (value: string, txState: TransactionState) => {
return tx return tx
} }
export const getTxFields = (tt?: string) => {
const txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === tt
);
if (!txFields) return {}
let _txFields = Object.keys(txFields)
.filter(
key => !["TransactionType", "Account", "Sequence"].includes(key)
)
.reduce<TxFields>(
(tf, key) => (
(tf[key as keyof TxFields] = (txFields as any)[key]), tf
),
{}
);
return _txFields
}
export { transactionsData } export { transactionsData }

View File

@@ -1,19 +1,29 @@
import { sign, XRPL_Account } from "xrpl-accountlib" import toast from 'react-hot-toast';
import state from "../state" import { derive, sign } from "xrpl-accountlib"
import state, { IAccount } from "../state"
// Mutate tx object with network estimated fee value const estimateFee = async (tx: Record<string, unknown>, account: IAccount, opts: { silent?: boolean } = {}): Promise<null | { base_fee: string, median_fee: string; minimum_fee: string; open_ledger_fee: string; }> => {
const estimateFee = async (tx: Record<string, unknown>, keypair: XRPL_Account): Promise<null | { base_fee: string, median_fee: string; minimum_fee: string; open_ledger_fee: string; }> => { try {
const copyTx = JSON.parse(JSON.stringify(tx)) const copyTx = JSON.parse(JSON.stringify(tx))
delete copyTx['SigningPubKey'] delete copyTx['SigningPubKey']
if (!copyTx.Fee) {
copyTx.Fee = '1000'
}
const keypair = derive.familySeed(account.secret)
const { signedTransaction } = sign(copyTx, keypair); const { signedTransaction } = sign(copyTx, keypair);
try {
const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction }) const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction })
if (res && res.drops) { if (res && res.drops) {
return tx['Fee'] = res.drops.base_fee; return res.drops;
} }
return null return null
} catch (err) { } catch (err) {
throw Error('Cannot estimate fee') if (!opts.silent) {
console.error(err)
toast.error("Cannot estimate fee.") // ? Some better msg
}
return null
} }
} }

View File

@@ -41,6 +41,7 @@ import hooksSkipHashBufLen from "./md/hooks-skip-hash-buf-len.md";
import hooksStateBufLen from "./md/hooks-state-buf-len.md"; import hooksStateBufLen from "./md/hooks-state-buf-len.md";
import hooksTransactionHashBufLen from "./md/hooks-transaction-hash-buf-len.md"; import hooksTransactionHashBufLen from "./md/hooks-transaction-hash-buf-len.md";
import hooksTransactionSlotLimit from "./md/hooks-transaction-slot-limit.md"; import hooksTransactionSlotLimit from "./md/hooks-transaction-slot-limit.md";
import hooksTrivialCbak from "./md/hooks-trivial-cbak.md";
import hooksValidateBufLen from "./md/hooks-validate-buf-len.md"; import hooksValidateBufLen from "./md/hooks-validate-buf-len.md";
import hooksVerifyBufLen from "./md/hooks-verify-buf-len.md"; import hooksVerifyBufLen from "./md/hooks-verify-buf-len.md";
@@ -90,6 +91,7 @@ const docs: { [key: string]: string; } = {
"hooks-state-buf-len": hooksStateBufLen, "hooks-state-buf-len": hooksStateBufLen,
"hooks-transaction-hash-buf-len": hooksTransactionHashBufLen, "hooks-transaction-hash-buf-len": hooksTransactionHashBufLen,
"hooks-transaction-slot-limit": hooksTransactionSlotLimit, "hooks-transaction-slot-limit": hooksTransactionSlotLimit,
"hooks-trivial-cbak": hooksTrivialCbak,
"hooks-validate-buf-len": hooksValidateBufLen, "hooks-validate-buf-len": hooksValidateBufLen,
"hooks-verify-buf-len": hooksVerifyBufLen, "hooks-verify-buf-len": hooksVerifyBufLen,
}; };

View File

@@ -1,7 +1,7 @@
# hooks-entry-points # hooks-entry-points
A Hook always implements and exports exactly two functions: [cbak](https://xrpl-hooks.readme.io/v2.0/reference/cbak) and [hook](https://xrpl-hooks.readme.io/v2.0/reference/hook). A Hook always implements and exports a [hook](https://xrpl-hooks.readme.io/v2.0/reference/hook) function.
This check shows error on translation units that do not have them. This check shows error on translation units that do not have it.
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks) [Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks)

View File

@@ -1,5 +1,5 @@
# hooks-hash-buf-len # hooks-hash-buf-len
Functions [util_sha512h](https://xrpl-hooks.readme.io/v2.0/reference/util_sha512h), [hook_hash](https://xrpl-hooks.readme.io/v2.0/reference/hook_hash), [ledger_last_hash](https://xrpl-hooks.readme.io/v2.0/reference/ledger_last_hash) and [nonce](https://xrpl-hooks.readme.io/v2.0/reference/nonce) have fixed-size hash output. Functions [util_sha512h](https://xrpl-hooks.readme.io/v2.0/reference/util_sha512h), [hook_hash](https://xrpl-hooks.readme.io/v2.0/reference/hook_hash), [ledger_last_hash](https://xrpl-hooks.readme.io/v2.0/reference/ledger_last_hash), [etxn_nonce](https://xrpl-hooks.readme.io/v2.0/reference/etxn_nonce) and [ledger_nonce](https://xrpl-hooks.readme.io/v2.0/reference/ledger_nonce) have fixed-size hash output.
This check warns about too-small size of their output buffer (if it's specified by a constant - variable parameter is ignored). This check warns about too-small size of their output buffer (if it's specified by a constant - variable parameter is ignored).

View File

@@ -0,0 +1,7 @@
# hooks-trivial-cbak
A Hook may implement and export a [cbak](https://xrpl-hooks.readme.io/v2.0/reference/cbak) function.
But the function is optional, and defining it so that it doesn't do anything besides returning a constant value is unnecessary (except for some debugging scenarios) and just increases the hook size. This check warns about such implementations.
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/compiling-hooks)