Compare commits

..

9 Commits

Author SHA1 Message Date
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
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
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
muzam1l
704ebe4b92 Show tx hash instead of server ledger index in deploy log. 2022-05-26 14:55:24 +05:30
7 changed files with 233 additions and 93 deletions

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { FC } from "react";
import { FC, useCallback, useState } from "react";
import Container from "../Container";
import Flex from "../Flex";
import Input from "../Input";
@@ -9,17 +9,26 @@ import {
TransactionState,
transactionsData,
TxFields,
getTxFields,
} from "../../state/transactions";
import { useSnapshot } from "valtio";
import state from "../../state";
import { streamState } from "../DebugStream";
import { Button } from "..";
interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => void;
setState: (
pTx?: Partial<TransactionState> | undefined
) => TransactionState | undefined;
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 {
selectedAccount,
@@ -45,32 +54,54 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
}))
.filter(acc => acc.value !== selectedAccount?.value);
const resetOptions = (tt: string) => {
const txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === tt
);
const [feeLoading, setFeeLoading] = useState(false);
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 resetOptions = useCallback(
(tt: string) => {
const fields = getTxFields(tt);
if (!fields.Destination) setState({ selectedDestAccount: null });
return setState({ txFields: fields });
},
[setState]
);
const handleSetAccount = (acc: SelectOption) => {
setState({ 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) => {
setState({ selectedTransaction: tt });
resetOptions(tt.value);
const newState = resetOptions(tt.value);
handleEstimateFee(newState, true);
};
const specialFields = ["TransactionType", "Account", "Destination"];
@@ -87,7 +118,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
height: "calc(100% - 45px)",
}}
>
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
<Flex column fluid css={{ height: "100%", overflowY: "auto", pr: "$1" }}>
<Flex
row
fluid
@@ -174,36 +205,49 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
}
let isXrp = typeof _value === "object" && _value.$type === "xrp";
const isFee = field === "Fee";
return (
<Flex
key={field}
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
mb: "$3",
pr: "1px",
}}
>
<Text muted css={{ mr: "$3" }}>
{field + (isXrp ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e => {
setState({
txFields: {
...txFields,
[field]:
typeof _value === "object"
? { ..._value, $value: e.target.value }
: e.target.value,
},
});
<Flex column key={field} css={{ mb: "$2", pr: "1px" }}>
<Flex
row
fluid
css={{
justifyContent: "flex-end",
alignItems: "center",
position: "relative",
}}
css={{ width: "70%", flex: "inherit" }}
/>
>
<Text muted css={{ mr: "$3" }}>
{field + (isXrp ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e => {
handleSetField(field, e.target.value);
}}
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>
);
})}

View File

@@ -160,14 +160,14 @@ export const deployHook = async (
message: ref(
<>
[{submitRes.engine_result}] {submitRes.engine_result_message}{" "}
Validated ledger index:{" "}
Transaction hash:{" "}
<Link
as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.validated_ledger_index}`}
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.tx_json?.hash}`}
target="_blank"
rel="noopener noreferrer"
>
{submitRes.validated_ledger_index}
{submitRes.tx_json?.hash}
</Link>
</>
),

View File

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

View File

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

View File

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