Add fee estimate button to fee and update the deploying

This commit is contained in:
Valtteri Karesto
2022-05-25 13:05:04 +03:00
parent 864711697b
commit 933bdb5968
2 changed files with 313 additions and 215 deletions

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,176 +53,263 @@ export type SetHookData = {
// }[]; // }[];
}; };
export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => { export const SetHookDialog: React.FC<{ accountIndex: number }> = React.memo(
const snap = useSnapshot(state); ({ accountIndex }) => {
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false); const snap = useSnapshot(state);
const { const account = snap.accounts[accountIndex];
register, const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
handleSubmit, const {
control, register,
watch, handleSubmit,
setValue, control,
formState: { errors }, watch,
} = useForm<SetHookData>({ setValue,
defaultValues: { getValues,
HookNamespace: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "", formState: { errors },
}, } = useForm<SetHookData>({
}); defaultValues: {
const { fields, append, remove } = useFieldArray({ HookNamespace:
control, snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
name: "HookParameters", // unique name for your Field Array Invoke: transactionOptions.filter((to) => to.label === "ttPAYMENT"),
}); },
});
const { fields, append, remove } = useFieldArray({
control,
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(() => {
setValue( setValue(
"HookNamespace", "HookNamespace",
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "" snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
); );
}, [snap.activeWat, snap.files, setValue]); setFormInitialized(true);
// const { }, [snap.activeWat, snap.files, setValue]);
// fields: grantFields, // Calcucate initial fee estimate when modal opens
// append: grantAppend, useEffect(() => {
// remove: grantRemove, if (formInitialized) {
// } = useFieldArray({ (async () => {
// control, const formValues = getValues();
// name: "HookGrants", // unique name for your Field Array const tx = await prepareDeployHookTx(account, formValues);
// }); if (!tx) {
const [hashedNamespace, setHashedNamespace] = useState(""); return;
const namespace = watch(
"HookNamespace",
snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
);
const calculateHashedValue = useCallback(async () => {
const hashedVal = await sha256(namespace);
setHashedNamespace(hashedVal.toUpperCase());
}, [namespace]);
useEffect(() => {
calculateHashedValue();
}, [namespace, calculateHashedValue]);
if (!account) {
return null;
}
const onSubmit: SubmitHandler<SetHookData> = async (data) => {
const currAccount = state.accounts.find(
(acc) => acc.address === account.address
);
if (currAccount) currAccount.isLoading = true;
const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false;
if (res && res.engine_result === "tesSUCCESS") {
toast.success("Transaction succeeded!");
return setIsSetHookDialogOpen(false);
}
toast.error(`Transaction failed! (${res?.engine_result_message})`);
};
return (
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
<DialogTrigger asChild>
<Button
ghost
size="xs"
uppercase
variant={"secondary"}
disabled={
account.isLoading ||
!snap.files.filter((file) => file.compiledWatContent).length
} }
> const res = await estimateFee(tx, account);
Set Hook if (res && res.base_fee) {
</Button> setValue("Fee", res.base_fee);
</DialogTrigger> }
<DialogContent> })();
<form onSubmit={handleSubmit(onSubmit)}> }
<DialogTitle>Deploy configuration</DialogTitle> }, [formInitialized]);
<DialogDescription as="div"> // const {
<Stack css={{ width: "100%", flex: 1 }}> // fields: grantFields,
<Box css={{ width: "100%" }}> // append: grantAppend,
<Label>Invoke on transactions</Label> // remove: grantRemove,
<Controller // } = useFieldArray({
name="Invoke" // control,
control={control} // name: "HookGrants", // unique name for your Field Array
defaultValue={transactionOptions.filter( // });
(to) => to.label === "ttPAYMENT" const [hashedNamespace, setHashedNamespace] = useState("");
)} const namespace = watch(
render={({ field }) => ( "HookNamespace",
<Select snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
{...field} );
closeMenuOnSelect={false} const calculateHashedValue = useCallback(async () => {
isMulti const hashedVal = await sha256(namespace);
menuPosition="fixed" setHashedNamespace(hashedVal.toUpperCase());
options={transactionOptions} }, [namespace]);
/> useEffect(() => {
)} calculateHashedValue();
/> }, [namespace, calculateHashedValue]);
</Box>
<Box css={{ width: "100%" }}> if (!account) {
<Label>Hook Namespace Seed</Label> return null;
<Input }
{...register("HookNamespace", { required: true })}
autoComplete={"off"} const onSubmit: SubmitHandler<SetHookData> = async (data) => {
defaultValue={ const currAccount = state.accounts.find(
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "" (acc) => acc.address === account.address
} );
/> if (currAccount) currAccount.isLoading = true;
{errors.HookNamespace?.type === "required" && ( const res = await deployHook(account, data);
<Box css={{ display: "inline", color: "$red11" }}> if (currAccount) currAccount.isLoading = false;
Namespace is required
</Box> if (res && res.engine_result === "tesSUCCESS") {
)} toast.success("Transaction succeeded!");
<Box css={{ mt: "$3" }}> return setIsSetHookDialogOpen(false);
<Label>Hook Namespace (sha256)</Label> }
<Input readOnly value={hashedNamespace} /> toast.error(`Transaction failed! (${res?.engine_result_message})`);
};
return (
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
<DialogTrigger asChild>
<Button
ghost
size="xs"
uppercase
variant={"secondary"}
disabled={
account.isLoading ||
!snap.files.filter((file) => file.compiledWatContent).length
}
>
Set Hook
</Button>
</DialogTrigger>
<DialogContent>
<form onSubmit={handleSubmit(onSubmit)}>
<DialogTitle>Deploy configuration</DialogTitle>
<DialogDescription as="div">
<Stack css={{ width: "100%", flex: 1 }}>
<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}
closeMenuOnSelect={false}
isMulti
menuPosition="fixed"
options={transactionOptions}
/>
)}
/>
</Box> </Box>
</Box> <Box css={{ width: "100%" }}>
<Box css={{ width: "100%" }}> <Label>Hook Namespace Seed</Label>
<Label style={{ marginBottom: "10px", display: "block" }}> <Input
Hook parameters {...register("HookNamespace", { required: true })}
</Label> autoComplete={"off"}
<Stack> defaultValue={
{fields.map((field, index) => ( snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
<Stack key={field.id}>
<Input
// important to include key with field's id
placeholder="Parameter name"
{...register(
`HookParameters.${index}.HookParameter.HookParameterName`
)}
/>
<Input
placeholder="Value (hex-quoted)"
{...register(
`HookParameters.${index}.HookParameter.HookParameterValue`
)}
/>
<Button onClick={() => remove(index)} variant="destroy">
<Trash weight="regular" size="16px" />
</Button>
</Stack>
))}
<Button
outline
fullWidth
type="button"
onClick={() =>
append({
HookParameter: {
HookParameterName: "",
HookParameterValue: "",
},
})
} }
> />
<Plus size="16px" /> {errors.HookNamespace?.type === "required" && (
Add Hook Parameter <Box css={{ display: "inline", color: "$red11" }}>
</Button> Namespace is required
</Stack> </Box>
</Box> )}
{/* <Box css={{ width: "100%" }}> <Box css={{ mt: "$3" }}>
<Label>Hook Namespace (sha256)</Label>
<Input readOnly value={hashedNamespace} />
</Box>
</Box>
<Box css={{ width: "100%" }}>
<Label style={{ marginBottom: "10px", display: "block" }}>
Hook parameters
</Label>
<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`
)}
/>
<Input
placeholder="Value (hex-quoted)"
{...register(
`HookParameters.${index}.HookParameter.HookParameterValue`
)}
/>
<Button onClick={() => remove(index)} variant="destroy">
<Trash weight="regular" size="16px" />
</Button>
</Stack>
))}
<Button
outline
fullWidth
type="button"
onClick={() =>
append({
HookParameter: {
HookParameterName: "",
HookParameterValue: "",
},
})
}
>
<Plus size="16px" />
Add Hook Parameter
</Button>
</Stack>
</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%" }}>
<label style={{ marginBottom: "10px", display: "block" }}> <label style={{ marginBottom: "10px", display: "block" }}>
Hook Grants Hook Grants
</label> </label>
@@ -269,38 +357,39 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
</Button> </Button>
</Stack> </Stack>
</Box> */} </Box> */}
</Stack> </Stack>
</DialogDescription> </DialogDescription>
<Flex <Flex
css={{ css={{
marginTop: 25, marginTop: 25,
justifyContent: "flex-end", justifyContent: "flex-end",
gap: "$3", gap: "$3",
}} }}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
{/* <DialogClose asChild> */}
<Button
variant="primary"
type="submit"
isLoading={account.isLoading}
> >
Set Hook <DialogClose asChild>
</Button> <Button outline>Cancel</Button>
{/* </DialogClose> */} </DialogClose>
</Flex> {/* <DialogClose asChild> */}
<DialogClose asChild> <Button
<Box css={{ position: "absolute", top: "$3", right: "$3" }}> variant="primary"
<X size="20px" /> type="submit"
</Box> isLoading={account.isLoading}
</DialogClose> >
</form> Set Hook
</DialogContent> </Button>
</Dialog> {/* </DialogClose> */}
); </Flex>
}; <DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</form>
</DialogContent>
</Dialog>
);
}
);
export default SetHookDialog; export default SetHookDialog;

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); /* deployHook function turns the wasm binary into
try { * hex string, signs the transaction and deploys it to
// Update tx Fee value with network estimation * Hooks testnet.
await estimateFee(tx, keypair); */
} catch (err) { export const deployHook = async (
// use default value what you defined earlier account: IAccount & { name?: string },
console.log(err); 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);