diff --git a/public/locales/en.json b/public/locales/en.json index 207f790..c4b8d14 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -995,6 +995,7 @@ "categoryExactName": "Exact name", "addressHint": "Balance: <1 />", + "addressHintWithStake": "Balance: <1 /> Stake: <3 />", "addressHintWithNames": "Names: <1>{{names, number}}", "nameHint": "Owner: <1 />", "nameHintNotFound": "Name not found.", diff --git a/src/components/addresses/picker/AddressHint.tsx b/src/components/addresses/picker/AddressHint.tsx index 6c4d0e1..ef8a312 100644 --- a/src/components/addresses/picker/AddressHint.tsx +++ b/src/components/addresses/picker/AddressHint.tsx @@ -9,9 +9,10 @@ interface Props { address?: TenebraAddressWithNames; nameHint?: boolean; + stake?: number; } -export function AddressHint({ address, nameHint }: Props): JSX.Element { +export function AddressHint({ address, nameHint, stake }: Props): JSX.Element { const { t } = useTranslation(); return @@ -23,10 +24,19 @@ ) : ( - // Otherwise, show the balance - - Balance: - + stake ? + // Otherwise, show the balance + ( + + Balance: Stake: + + ) + : + ( + + Balance: + + ) ) } ; diff --git a/src/components/addresses/picker/AddressPicker.tsx b/src/components/addresses/picker/AddressPicker.tsx index c4abdac..ad196fc 100644 --- a/src/components/addresses/picker/AddressPicker.tsx +++ b/src/components/addresses/picker/AddressPicker.tsx @@ -34,6 +34,7 @@ otherPickerValue?: string; walletsOnly?: boolean; + showStake?: boolean; noWallets?: boolean; noNames?: boolean; nameHint?: boolean; @@ -57,6 +58,7 @@ otherPickerValue, walletsOnly, + showStake, noWallets, noNames, nameHint, @@ -129,7 +131,7 @@ // Fetch an address or name hint if possible const { pickerHints, foundName } = usePickerHints( - nameHint, cleanValue, hasExactName, suppressUpdates + nameHint, cleanValue, hasExactName, suppressUpdates, showStake ); // Re-validate this field if the picker hints foundName changed diff --git a/src/components/addresses/picker/PickerHints.tsx b/src/components/addresses/picker/PickerHints.tsx index b8bee93..f3573be 100644 --- a/src/components/addresses/picker/PickerHints.tsx +++ b/src/components/addresses/picker/PickerHints.tsx @@ -10,7 +10,7 @@ import { useWallets } from "@wallets"; import * as api from "@api"; -import { TenebraAddressWithNames, lookupAddress } from "@api/lookup"; +import { TenebraAddressWithNames, lookupAddress, lookupStakes } from "@api/lookup"; import { TenebraName } from "@api/types"; import { WalletHint } from "./WalletHint"; @@ -38,7 +38,8 @@ nameHint?: boolean, value?: string, hasExactName?: boolean, - suppressUpdates?: boolean + suppressUpdates?: boolean, + showStake?: boolean ): PickerHintsRes { // Used for clean-up const isMounted = useRef(true); @@ -48,6 +49,7 @@ // Handle showing an address or name hint if the value is valid const [foundAddress, setFoundAddress] = useState(); + const [foundStake, setFoundStake] = useState(); const [foundName, setFoundName] = useState(); // To auto-refresh address balances, we need to subscribe to the address. @@ -69,7 +71,8 @@ value: string, hasAddress?: boolean, hasName?: boolean, - nameHint?: boolean + nameHint?: boolean, + showStake?: boolean ) => { // Skip doing anything when unmounted to avoid illegal state updates if (!isMounted.current) return debug("unmounted skipped lookupHint"); @@ -87,10 +90,16 @@ if (!isMounted.current) return debug("unmounted skipped lookupHint hasAddress try"); setFoundAddress(address); + if (showStake) { + const lookupStakeResults = await lookupStakes([value]); + const tenebraStake = lookupStakeResults[value]; + setFoundStake(tenebraStake?.stake); + } } catch (ignored) { if (!isMounted.current) return debug("unmounted skipped lookupHint hasAddress catch"); setFoundAddress(false); + setFoundStake(false); } } else if (hasName) { // Lookup a name @@ -122,6 +131,7 @@ if (!value) { setFoundAddress(undefined); setFoundName(undefined); + setFoundStake(undefined); setValidAddress(undefined); return; } @@ -143,11 +153,8 @@ } // Perform the lookup (debounced) - lookupHint(nameSuffix, value, hasValidAddress, hasExactName, nameHint); - }, [ - lookupHint, nameSuffix, value, addressPrefix, hasExactName, nameHint, - validAddress, lastTransactionID, joinedAddressList, suppressUpdates - ]); + lookupHint(nameSuffix, value, hasValidAddress, hasExactName, nameHint, showStake); + }, [lookupHint, nameSuffix, value, addressPrefix, hasExactName, nameHint, validAddress, lastTransactionID, joinedAddressList, suppressUpdates, showStake]); // Clean up the debounced function when unmounting useEffect(() => { @@ -191,7 +198,7 @@ {/* Show an address hint if possible */} {showAddressHint && ( - + )} {/* Show a name hint if possible */} diff --git a/src/components/tenebra/TenebraValue.tsx b/src/components/tenebra/TenebraValue.tsx index bb72425..cf4e59b 100644 --- a/src/components/tenebra/TenebraValue.tsx +++ b/src/components/tenebra/TenebraValue.tsx @@ -41,9 +41,9 @@ return ( - {icon || ((currencySymbol || "KST") === "KST" && )} + {icon || ((currencySymbol || "TST") === "TST" && )} {(value || 0).toLocaleString()} - {long && {currencySymbol || "KST"}} + {long && {currencySymbol || "TST"}} ); }; diff --git a/src/components/transactions/AmountInput.tsx b/src/components/transactions/AmountInput.tsx index 0fe1012..7d6ca63 100644 --- a/src/components/transactions/AmountInput.tsx +++ b/src/components/transactions/AmountInput.tsx @@ -10,6 +10,13 @@ import { useCurrency } from "@utils/tenebra"; import { TenebraSymbol } from "@comp/tenebra/TenebraSymbol"; +import { StakingActionType } from "@pages/staking/StakingForm"; + +export interface StakingFormValues { + from: string; + action: StakingActionType; + amount: number; +} interface Props { from?: string; @@ -18,6 +25,7 @@ label?: ReactNode; required?: boolean; disabled?: boolean; + stakingFormValues?: Partial; tabIndex?: number; } @@ -29,6 +37,7 @@ label, required, disabled, + stakingFormValues, tabIndex, ...props @@ -44,7 +53,11 @@ function onClickMax() { if (!from) return; const currentWallet = walletAddressMap[from]; - setAmount(currentWallet?.balance || 0); + if (!stakingFormValues || (stakingFormValues.action && stakingFormValues.action === "deposit")) { + setAmount(currentWallet?.balance || 0); + } else if (stakingFormValues.action && stakingFormValues.action === "withdraw") { + setAmount(currentWallet?.stake || 0); + } } const amountRequired = required === undefined || !!required; @@ -57,7 +70,7 @@ {/* Prepend the Tenebra symbol if possible. Note that ant's InputNumber * doesn't support addons, so this has to be done manually. */} - {(currency_symbol || "KST") === "KST" && ( + {(currency_symbol || "TST") === "TST" && ( @@ -91,8 +104,13 @@ const currentWallet = walletAddressMap[from]; if (!currentWallet) return; - if (value > (currentWallet.balance || 0)) - throw t("sendTransaction.errorAmountTooHigh"); + if (!stakingFormValues || (stakingFormValues.action && stakingFormValues.action === "deposit")) { + if (value > (currentWallet.balance || 0)) + throw t("sendTransaction.errorAmountTooHigh"); + } else if (stakingFormValues.action && stakingFormValues.action === "withdraw") { + if (value > (currentWallet.stake || 0)) + throw t("sendTransaction.errorAmountTooHigh"); + } } }, ]} @@ -108,7 +126,7 @@ {/* Currency suffix */} - {currency_symbol || "KST"} + {currency_symbol || "TST"} {/* Max value button */} diff --git a/src/pages/staking/StakingForm.tsx b/src/pages/staking/StakingForm.tsx index 994236d..bec8c12 100644 --- a/src/pages/staking/StakingForm.tsx +++ b/src/pages/staking/StakingForm.tsx @@ -25,7 +25,7 @@ import { useAuthFailedModal } from "@api/AuthFailed"; import { AddressPicker } from "@comp/addresses/picker/AddressPicker"; -import { AmountInput } from "@comp/transactions/AmountInput"; +import { AmountInput, StakingFormValues } from "@comp/transactions/AmountInput"; import { StakingConfirmModalContents } from "./StakingConfirmModal"; import awaitTo from "await-to-js"; @@ -39,16 +39,10 @@ export type StakingActionType = "deposit" | "withdraw"; -export interface FormValues { - from: string; - action: StakingActionType; - amount: number; -} - interface Props { from?: Wallet | string; amount?: number; - form: FormInstance; + form: FormInstance; triggerSubmit: () => Promise; } @@ -79,6 +73,8 @@ const [from, setFrom] = useState(initialFrom); + const [formValues, setFormValues] = useState>(); + // Focus the 'to' input on initial render const toRef = useRef(null); useMountEffect(() => { @@ -86,10 +82,11 @@ }); function onValuesChange( - changed: Partial, - values: Partial + changed: Partial, + values: Partial ) { setFrom(values.from || ""); + setFormValues(values); // Update and save the lastTxFrom so the next time the modal is opened // it will remain on this address @@ -103,7 +100,7 @@ } } - const initialValues: FormValues = useMemo(() => ({ + const initialValues: StakingFormValues = useMemo(() => ({ from: initialFrom, amount: initialAmount || 1, action: "deposit" @@ -117,6 +114,7 @@ // If the initial values change, refresh the form useEffect(() => { form?.setFieldsValue(initialValues); + setFormValues(initialValues); }, [form, initialValues]); return
form.setFieldsValue({ amount })} + stakingFormValues={formValues} tabIndex={3} /> ; @@ -179,7 +179,7 @@ } interface StakingFormHookResponse { - form: FormInstance; + form: FormInstance; triggerSubmit: () => Promise; triggerReset: () => void; isSubmitting: boolean; @@ -195,7 +195,7 @@ }: StakingFormHookProps = {}): StakingFormHookResponse { const { t } = useTranslation(); - const [form] = Form.useForm(); + const [form] = Form.useForm(); const [isSubmitting, setIsSubmitting] = useState(false); // Used to check for warning on large transactions @@ -222,7 +222,7 @@ // Take the form values and known wallet and submit the transaction async function submitStakingTransaction( - { amount, action }: FormValues, + { amount, action }: StakingFormValues, wallet: Wallet ): Promise { // Manually get the master password from the store state, because this might diff --git a/src/pages/staking/StakingPage.less b/src/pages/staking/StakingPage.less index e8eae64..c8de286 100644 --- a/src/pages/staking/StakingPage.less +++ b/src/pages/staking/StakingPage.less @@ -1,7 +1,7 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of TenebraWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/TenebraWeb2/blob/master/LICENSE.txt -@import (reference) "../../../App.less"; +@import (reference) "../../App.less"; .staking-page { .staking-container { @@ -10,7 +10,7 @@ margin: 0 auto; width: 100%; - max-width: 768px; + max-width: 1024px; background: @kw-light; border-radius: @kw-big-card-border-radius; diff --git a/src/tenebra/api/lookup.ts b/src/tenebra/api/lookup.ts index e68bb04..4262b7e 100644 --- a/src/tenebra/api/lookup.ts +++ b/src/tenebra/api/lookup.ts @@ -1,7 +1,7 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of TenebraWeb 2 under AGPL-3.0. // Full details: https://github.com/tmpim/TenebraWeb2/blob/master/LICENSE.txt -import { TenebraAddress, TenebraTransaction, TenebraName, TenebraBlock } from "./types"; +import { TenebraAddress, TenebraTransaction, TenebraName, TenebraBlock, TenebraStake } from "./types"; import * as api from "."; import { @@ -18,9 +18,15 @@ notFound: number; addresses: Record; } +interface LookupStakesResponse { + found: number; + notFound: number; + stakes: Record; +} export interface TenebraAddressWithNames extends TenebraAddress { names?: number } export type AddressLookupResults = Record; +export type StakeLookupResults = Record; export async function lookupAddresses( addresses: string[], @@ -43,6 +49,25 @@ return {}; } +export async function lookupStakes( + addresses: string[] +): Promise { + if (!addresses || addresses.length === 0) return {}; + + try { + const data = await api.get( + "lookup/stakes/" + + encodeURIComponent(addresses.join(",")) + ); + + return data.stakes; + } catch (err) { + criticalError(err); + } + + return {}; +} + /** Uses the lookup API to retrieve a single address. */ export async function lookupAddress( address: string, diff --git a/src/tenebra/api/types.ts b/src/tenebra/api/types.ts index 3fb2ad3..806504e 100644 --- a/src/tenebra/api/types.ts +++ b/src/tenebra/api/types.ts @@ -100,7 +100,7 @@ } export const DEFAULT_CURRENCY: TenebraCurrency = { address_prefix: "k", name_suffix: "kst", - currency_name: "Tenebra", currency_symbol: "KST" + currency_name: "Tenebra", currency_symbol: "TST" }; export interface TenebraMOTDBase { diff --git a/src/tenebra/wallets/Wallet.ts b/src/tenebra/wallets/Wallet.ts index f96565a..53ef9a0 100644 --- a/src/tenebra/wallets/Wallet.ts +++ b/src/tenebra/wallets/Wallet.ts @@ -20,6 +20,7 @@ // Fetched from API address: string; balance?: number; + stake?: number; names?: number; firstSeen?: string; lastSynced?: string; diff --git a/src/tenebra/wallets/functions/syncWallets.ts b/src/tenebra/wallets/functions/syncWallets.ts index 13a31ff..6fd4093 100644 --- a/src/tenebra/wallets/functions/syncWallets.ts +++ b/src/tenebra/wallets/functions/syncWallets.ts @@ -4,16 +4,18 @@ import { store } from "@app"; import * as actions from "@actions/WalletsActions"; -import { TenebraAddressWithNames, lookupAddresses } from "../../api/lookup"; +import { TenebraAddressWithNames, lookupAddresses, lookupStakes } from "../../api/lookup"; import { Wallet, saveWallet } from ".."; import Debug from "debug"; +import { TenebraStake } from "@api/types"; const debug = Debug("tenebraweb:sync-wallets"); function syncWalletProperties( wallet: Wallet, address: TenebraAddressWithNames | null, + stake: TenebraStake | null, syncTime: Date ): Wallet { if (address) { @@ -22,7 +24,8 @@ ...(address.balance !== undefined ? { balance: address.balance } : {}), ...(address.names !== undefined ? { names: address.names } : {}), ...(address.firstseen !== undefined ? { firstSeen: address.firstseen } : {}), - lastSynced: syncTime.toISOString() + lastSynced: syncTime.toISOString(), + stake: stake ? stake.stake : wallet.stake }; } else { // Wallet was unsyncable (address not found), clear its properties @@ -45,11 +48,13 @@ // Fetch the data from the sync node (e.g. balance) const { address } = wallet; const lookupResults = await lookupAddresses([address], true); + const lookupStakeResults = await lookupStakes([address]); debug("synced individual wallet %s (%s): %o", wallet.id, wallet.address, lookupResults); const tenebraAddress = lookupResults[address]; - syncWalletUpdate(wallet, tenebraAddress, dontSave); + const tenebraStake = lookupStakeResults[address]; + syncWalletUpdate(wallet, tenebraAddress, tenebraStake, dontSave); } /** Given an already synced wallet, save it to local storage, and dispatch the @@ -57,10 +62,11 @@ export function syncWalletUpdate( wallet: Wallet, address: TenebraAddressWithNames | null, + stake: TenebraStake | null, dontSave?: boolean ): void { const syncTime = new Date(); - const updatedWallet = syncWalletProperties(wallet, address, syncTime); + const updatedWallet = syncWalletProperties(wallet, address, stake, syncTime); // Save the wallet to local storage, unless this was an external sync action if (!dontSave) saveWallet(updatedWallet); @@ -84,12 +90,14 @@ // Fetch all the data from the sync node (e.g. balances) const addresses = Object.values(wallets).map(w => w.address); const lookupResults = await lookupAddresses(addresses, true); + const lookupStakeResults = await lookupStakes(addresses); // Create a WalletMap with the updated wallet properties const updatedWallets = Object.entries(wallets).map(([_, wallet]) => { const tenebraAddress = lookupResults[wallet.address]; if (!tenebraAddress) return wallet; // Skip unsyncable wallets - return syncWalletProperties(wallet, tenebraAddress, syncTime); + const tenebraStake = lookupStakeResults[wallet.address]; + return syncWalletProperties(wallet, tenebraAddress, tenebraStake, syncTime); }).reduce((o, wallet) => ({ ...o, [wallet.id]: wallet }), {}); // Save the wallets to local storage (unless dontSave is set)