diff --git a/public/locales/en.json b/public/locales/en.json
index 7088f68..207f790 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -45,6 +45,7 @@
"sort": "Sort results",
"settings": "Settings",
+ "staking": "Staking",
"more": "More"
},
@@ -70,6 +71,29 @@
"updateReload": "Reload"
},
+ "staking": {
+ "modalSubmit": "Submit",
+ "buttonSubmit": "Submit",
+ "deposit": "Deposit",
+ "withdraw": "Withdraw",
+ "errorInvalidQuery": "The query parameters were invalid, they were ignored.",
+ "errorUnknown": "Unknown error sending transaction. See console for details.",
+ "errorNotificationTitle": "Transaction failed",
+ "errorParameterAmount": "Invalid amount.",
+ "errorInsufficientFunds": "Insufficient funds in wallet or stake.",
+ "labelAction": "Action",
+ "labelWallet": "Staking wallet",
+ "stakeLargeConfirm": "Are you sure you want to stake <1 />?",
+ "stakeLargeConfirmHalf": "Are you sure you want to stake <1 />? This is over half your balance!",
+ "stakeLargeConfirmAll": "Are you sure you want to stake <1 />? This is your entire balance!",
+ "successNotificationTitle": "Transaction successful",
+ "successNotificationContent": "Your new stake balance is <1 /> after a staking action with <3 />.",
+ "successNotificationButton": "View transactions",
+ "modalTitle": "Staking",
+ "siteTitle": "Staking",
+ "title": "Staking"
+ },
+
"dialog": {
"close": "Close",
"yes": "Yes",
diff --git a/src/components/results/NoWalletsResult.tsx b/src/components/results/NoWalletsResult.tsx
index 5eeac1d..124b831 100644
--- a/src/components/results/NoWalletsResult.tsx
+++ b/src/components/results/NoWalletsResult.tsx
@@ -11,7 +11,7 @@
import { SmallResult } from "./SmallResult";
-export type ResultType = "transactions" | "names" | "sendTransaction";
+export type ResultType = "transactions" | "names" | "sendTransaction" | "staking";
interface Props {
type?: ResultType;
diff --git a/src/global/AppRouter.tsx b/src/global/AppRouter.tsx
index 3ac3ffc..eafee8e 100644
--- a/src/global/AppRouter.tsx
+++ b/src/global/AppRouter.tsx
@@ -10,6 +10,7 @@
import { SendTransactionPage } from "@pages/transactions/send/SendTransactionPage";
import { RequestPage } from "@pages/transactions/request/RequestPage";
+import { StakingPage } from "@pages/staking/StakingPage";
import { AddressPage } from "@pages/addresses/AddressPage";
import { BlocksPage } from "@pages/blocks/BlocksPage";
@@ -46,9 +47,10 @@
{ path: "/me/names", name: "myNames",
component: },
- // Payments
+ // Payments & Staking
{ path: "/send", name: "sendTransaction", component: },
{ path: "/request", name: "request", component: },
+ { path: "/staking", name: "staking", component: },
// Network explorer
{ path: "/network/addresses/:address", name: "address", component: },
diff --git a/src/layout/nav/AppHeader.tsx b/src/layout/nav/AppHeader.tsx
index 2649f8d..41b2660 100644
--- a/src/layout/nav/AppHeader.tsx
+++ b/src/layout/nav/AppHeader.tsx
@@ -2,7 +2,7 @@
// This file is part of TenebraWeb 2 under AGPL-3.0.
// Full details: https://github.com/tmpim/TenebraWeb2/blob/master/LICENSE.txt
import { Layout, Menu, Grid } from "antd";
-import { SendOutlined, DownloadOutlined, MenuOutlined } from "@ant-design/icons";
+import { SendOutlined, DownloadOutlined, MenuOutlined, BankOutlined } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
@@ -55,6 +55,13 @@
{t("nav.request")}
+
+ {/* Staking Tenebra */}
+
}>
+
+ {t("nav.staking")}
+
+
}
{/* Spacer to push search box to the right */}
diff --git a/src/pages/staking/QueryParamsHook.tsx b/src/pages/staking/QueryParamsHook.tsx
new file mode 100644
index 0000000..64d8a13
--- /dev/null
+++ b/src/pages/staking/QueryParamsHook.tsx
@@ -0,0 +1,40 @@
+// 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 { useMemo } from "react";
+
+import { useTFns } from "@utils/i18n";
+
+import { useLocation } from "react-router-dom";
+import { message } from "antd";
+
+interface Res {
+ amount?: number;
+}
+
+export function useStakingQuery(): Res {
+ const { tStr } = useTFns("staking.");
+ const search = useLocation().search;
+ // Memoise the query parsing, as notifications are triggered directly here. To
+ // avoid spamming them, this should only run once per query string.
+ return useMemo(() => {
+ const query = new URLSearchParams(search);
+
+ // Fetch the form parameters from the query string
+ const rawAmount = query.get("amount")?.trim();
+
+ const parsedAmount = rawAmount ? parseInt(rawAmount) : undefined;
+ const amountValid = rawAmount && !isNaN(parsedAmount!) && parsedAmount! > 0;
+
+ // Show a notification if any parameter is invalid
+ if (rawAmount && !amountValid) {
+ message.error(tStr("errorInvalidQuery"));
+ return {};
+ }
+
+ // The parameters were valid (or non-existent), return them
+ return {
+ amount: parsedAmount,
+ };
+ }, [tStr, search]);
+}
diff --git a/src/pages/staking/StakingConfirmModal.tsx b/src/pages/staking/StakingConfirmModal.tsx
new file mode 100644
index 0000000..9d286a6
--- /dev/null
+++ b/src/pages/staking/StakingConfirmModal.tsx
@@ -0,0 +1,30 @@
+// 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 { FC, Attributes } from "react";
+
+import { Trans } from "react-i18next";
+import { useTFns } from "@utils/i18n";
+
+import { TenebraValue } from "@comp/tenebra/TenebraValue";
+
+interface StakingConfirmModalContentsProps {
+ amount: number;
+ balance: number;
+}
+
+export const StakingConfirmModalContents: FC = ({ amount, balance, key2 }) => {
+ const { t, tKey } = useTFns("staking.");
+
+ // Show the appropriate message, if this is just over half the
+ // balance, or if it is the entire balance.
+ return = balance
+ ? "stakingLargeConfirmAll"
+ : "stakingLargeConfirmHalf"))}
+ >
+ Are you sure you want to send ?
+ This is over half your balance!
+ ;
+};
diff --git a/src/pages/staking/StakingForm.tsx b/src/pages/staking/StakingForm.tsx
new file mode 100644
index 0000000..994236d
--- /dev/null
+++ b/src/pages/staking/StakingForm.tsx
@@ -0,0 +1,366 @@
+// 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 { useState, useRef, useMemo, useEffect } from "react";
+import { Row, Col, Form, FormInstance, Modal } from "antd";
+import Select, { RefSelectProps } from "antd/lib/select";
+
+import { useTranslation } from "react-i18next";
+import { TranslatedError } from "@utils/i18n";
+
+import { useSelector, useDispatch } from "react-redux";
+import { store } from "@app";
+import { RootState } from "@store";
+import { setLastTxFrom } from "@actions/WalletsActions";
+
+import { useWallets, Wallet } from "@wallets";
+import { useMountEffect } from "@utils/hooks";
+import { sha256 } from "@utils/crypto";
+import { useBooleanSetting, useIntegerSetting } from "@utils/settings";
+
+import { useSyncNode } from "@api";
+import { TenebraStake } from "@api/types";
+import { makeDepositTransaction, makeWithdrawTransaction } from "@api/transactions";
+import { handleStakingError } from "./handleErrors";
+import { useAuthFailedModal } from "@api/AuthFailed";
+
+import { AddressPicker } from "@comp/addresses/picker/AddressPicker";
+import { AmountInput } from "@comp/transactions/AmountInput";
+import { StakingConfirmModalContents } from "./StakingConfirmModal";
+
+import awaitTo from "await-to-js";
+
+import Debug from "debug";
+const debug = Debug("tenebraweb:send-transaction-form");
+
+// This is from https://github.com/tmpim/Tenebra/blob/a924f3f/src/controllers/transactions.js#L102
+// except `+` is changed to `*`.
+export const METADATA_REGEXP = /^[\x20-\x7F\n]*$/i;
+
+export type StakingActionType = "deposit" | "withdraw";
+
+export interface FormValues {
+ from: string;
+ action: StakingActionType;
+ amount: number;
+}
+
+interface Props {
+ from?: Wallet | string;
+ amount?: number;
+ form: FormInstance;
+ triggerSubmit: () => Promise;
+}
+
+function StakingForm({
+ from: rawInitialFrom,
+ amount: initialAmount,
+ form,
+ triggerSubmit
+}: Props): JSX.Element {
+ const { t } = useTranslation();
+
+ // Get the initial wallet to show for the 'from' field. Use the provided
+ // wallet if we were given one, otherwise use the saved 'last wallet',
+ // or the first wallet we can find.
+ const initialFromAddress = typeof rawInitialFrom === "string"
+ ? rawInitialFrom : rawInitialFrom?.address;
+
+ const { addressList, walletAddressMap } = useWallets();
+ const firstWallet = addressList[0];
+
+ // Validate the lastTxFrom wallet still exists
+ const dispatch = useDispatch();
+ const lastTxFrom = useSelector((s: RootState) => s.wallets.lastTxFrom);
+ const lastTxFromAddress = lastTxFrom && addressList.includes(lastTxFrom)
+ ? lastTxFrom : undefined;
+
+ const initialFrom = initialFromAddress || lastTxFromAddress || firstWallet;
+
+ const [from, setFrom] = useState(initialFrom);
+
+ // Focus the 'to' input on initial render
+ const toRef = useRef(null);
+ useMountEffect(() => {
+ toRef?.current?.focus();
+ });
+
+ function onValuesChange(
+ changed: Partial,
+ values: Partial
+ ) {
+ setFrom(values.from || "");
+
+ // Update and save the lastTxFrom so the next time the modal is opened
+ // it will remain on this address
+ if (changed.from) {
+ const currentWallet = walletAddressMap[changed.from];
+ if (currentWallet && currentWallet.address !== lastTxFromAddress) {
+ debug("updating lastTxFrom to %s", currentWallet.address);
+ dispatch(setLastTxFrom(currentWallet));
+ localStorage.setItem("lastTxFrom", currentWallet.address);
+ }
+ }
+ }
+
+ const initialValues: FormValues = useMemo(() => ({
+ from: initialFrom,
+ amount: initialAmount || 1,
+ action: "deposit"
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }), [
+ rawInitialFrom,
+ initialFrom,
+ initialAmount
+ ]);
+
+ // If the initial values change, refresh the form
+ useEffect(() => {
+ form?.setFieldsValue(initialValues);
+ }, [form, initialValues]);
+
+ return ;
+}
+
+interface StakingFormHookProps {
+ from?: Wallet | string;
+ amount?: number;
+ onError?: (err: Error) => void;
+ onSuccess?: (stake: TenebraStake) => void;
+ allowClearOnSend?: boolean;
+}
+
+interface StakingFormHookResponse {
+ form: FormInstance;
+ triggerSubmit: () => Promise;
+ triggerReset: () => void;
+ isSubmitting: boolean;
+ stakingForm: JSX.Element;
+}
+
+export function useStakingForm({
+ from: initialFrom,
+ amount: initialAmount,
+ onError,
+ onSuccess,
+ allowClearOnSend
+}: StakingFormHookProps = {}): StakingFormHookResponse {
+ const { t } = useTranslation();
+
+ const [form] = Form.useForm();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // Used to check for warning on large transactions
+ const { walletAddressMap } = useWallets();
+ const url = useSyncNode();
+
+ // Confirmation modal used for when the transaction amount is very large.
+ // This is created here to provide a translation context for the modal.
+ const [confirmModal, contextHolder] = Modal.useModal();
+ // Modal used when auth fails
+ const { showAuthFailed, authFailedContextHolder } = useAuthFailedModal();
+
+ // If the form allows it, and the setting is enabled, clear the form when
+ // sending a transaction.
+ const confirmOnSend = useBooleanSetting("confirmTransactions");
+ const clearOnSend = useBooleanSetting("clearTransactionForm");
+ const sendDelay = useIntegerSetting("sendTransactionDelay");
+
+ // Called when the modal is closed
+ function onReset() {
+ form.resetFields();
+ setIsSubmitting(false);
+ }
+
+ // Take the form values and known wallet and submit the transaction
+ async function submitStakingTransaction(
+ { amount, action }: FormValues,
+ wallet: Wallet
+ ): Promise {
+ // Manually get the master password from the store state, because this might
+ // get called immediately after an auth, which doesn't give the hook time to
+ // update this submitTransaction function. The password here is used to
+ // decrypt the wallet to make the transaction.
+ const masterPassword = store.getState().masterPassword.masterPassword;
+ if (!masterPassword)
+ throw new TranslatedError("sendTransaction.errorWalletDecrypt");
+
+ // API errors will be bubbled up to the caller
+ if (action === "deposit") {
+ const stake = await makeDepositTransaction(
+ masterPassword,
+ wallet,
+ amount,
+ );
+
+ // Intentionally delay transaction submission, to prevent accidental double
+ // clicks on fast networks.
+ if (sendDelay > 0)
+ await (() => new Promise(resolve => setTimeout(resolve, sendDelay)))();
+
+ // Clear the form if the setting for it is enabled
+ if (allowClearOnSend && clearOnSend)
+ form.resetFields();
+
+ onSuccess?.(stake);
+ } else {
+ const stake = await makeWithdrawTransaction(
+ masterPassword,
+ wallet,
+ amount,
+ );
+
+ // Intentionally delay transaction submission, to prevent accidental double
+ // clicks on fast networks.
+ if (sendDelay > 0)
+ await (() => new Promise(resolve => setTimeout(resolve, sendDelay)))();
+
+ // Clear the form if the setting for it is enabled
+ if (allowClearOnSend && clearOnSend)
+ form.resetFields();
+
+ onSuccess?.(stake);
+ }
+
+ }
+
+ // Convert API errors to friendlier errors
+ const handleError = handleStakingError.bind(handleStakingError,
+ onError, showAuthFailed);
+
+ async function onSubmit() {
+ setIsSubmitting(true);
+
+ // Get the form values
+ const [err, values] = await awaitTo(form.validateFields());
+ if (err || !values) {
+ // Validation errors are handled by the form
+ setIsSubmitting(false);
+ return;
+ }
+
+ // Find the wallet we're trying to pay from, and verify it actually exists
+ // and has a balance (shouldn't happen)
+ const [err2, currentWallet] = await awaitTo((async () => {
+ const currentWallet = walletAddressMap[values.from];
+ if (!currentWallet)
+ throw new TranslatedError("sendTransaction.errorWalletGone");
+ if (!currentWallet.balance)
+ throw new TranslatedError("sendTransaction.errorAmountTooHigh");
+
+ return currentWallet;
+ })());
+
+ // Push out any errors with the wallet
+ if (err2 || !currentWallet?.balance) {
+ onError?.(err2!);
+ setIsSubmitting(false);
+ return;
+ }
+
+ // If the transaction is large (over half the balance), prompt for
+ // confirmation before sending
+ const { amount } = values;
+ // TODO: Anti-midiocy here but I'm too lazy to figure out what it does
+ const confirmable = await sha256(url) !== "cadc9145658308ead9ade59730063772f9a4d682650842981d3c075c5240cfee";
+ const showConfirm = confirmOnSend || confirmable;
+ const isLarge = amount >= currentWallet.balance / 2;
+ if (showConfirm || isLarge) {
+ // It's large, prompt for confirmation
+ confirmModal.confirm({
+ title: t("staking.modalTitle"),
+ content: ,
+
+ // Transaction looks OK, submit it
+ okText: t("staking.buttonSubmit"),
+ onOk: () => submitStakingTransaction(values, currentWallet)
+ .catch(err => handleError(err, currentWallet))
+ .finally(() => setIsSubmitting(false)),
+
+ cancelText: t("dialog.cancel"),
+ onCancel: () => setIsSubmitting(false)
+ });
+ } else {
+ // Transaction looks OK, submit it
+ submitStakingTransaction(values, currentWallet)
+ .catch(err => handleError(err, currentWallet))
+ .finally(() => setIsSubmitting(false));
+ }
+ }
+
+ // Create the transaction form instance here to be rendered by the caller
+ const stakingForm = <>
+
+
+ {/* Give the modals somewhere to find the context from. */}
+ {contextHolder}
+ {authFailedContextHolder}
+ >;
+
+ return {
+ form,
+ triggerSubmit: onSubmit,
+ triggerReset: onReset,
+ isSubmitting,
+ stakingForm
+ };
+}
diff --git a/src/pages/staking/StakingPage.less b/src/pages/staking/StakingPage.less
new file mode 100644
index 0000000..e8eae64
--- /dev/null
+++ b/src/pages/staking/StakingPage.less
@@ -0,0 +1,29 @@
+// 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";
+
+.staking-page {
+ .staking-container {
+ //display: flex;
+ flex-direction: column;
+
+ margin: 0 auto;
+ width: 100%;
+ max-width: 768px;
+
+ background: @kw-light;
+ border-radius: @kw-big-card-border-radius;
+
+ padding: @padding-lg;
+
+ .staking-submit {
+ float: right;
+ }
+ }
+
+ .staking-alert {
+ max-width: 768px;
+ margin: 0 auto @margin-md auto;
+ }
+}
diff --git a/src/pages/staking/StakingPage.tsx b/src/pages/staking/StakingPage.tsx
new file mode 100644
index 0000000..487a654
--- /dev/null
+++ b/src/pages/staking/StakingPage.tsx
@@ -0,0 +1,114 @@
+// 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 { useState } from "react";
+import { Button, Alert } from "antd";
+import { SendOutlined } from "@ant-design/icons";
+
+import { useTranslation } from "react-i18next";
+import { translateError } from "@utils/i18n";
+
+import { PageLayout } from "@layout/PageLayout";
+
+import { useWallets } from "@wallets";
+import { NoWalletsResult } from "@comp/results/NoWalletsResult";
+import { AuthorisedAction } from "@comp/auth/AuthorisedAction";
+
+import { TenebraStake } from "@api/types";
+
+import { useStakingForm } from "./StakingForm";
+import { NotifSuccessContents, NotifSuccessButton } from "./Success";
+import { useStakingQuery } from "./QueryParamsHook";
+
+import "./StakingPage.less";
+
+export function StakingPage(): JSX.Element {
+ const { t } = useTranslation();
+
+ // The success or error alert
+ const [alert, setAlert] = useState(null);
+
+ // Get any pre-filled values from the query parameters
+ const { amount } = useStakingQuery();
+
+ // Create the transaction form
+ const { isSubmitting, triggerSubmit, stakingForm } = useStakingForm({
+ amount,
+ onSuccess: stake => setAlert(),
+ onError: err => setAlert()
+ });
+
+ // Don't show the form if there are no wallets.
+ const { addressList } = useWallets();
+ const hasWallets = addressList?.length > 0;
+
+ function onSubmit() {
+ // Close the alert before submission, to forcibly move the form
+ setAlert(null);
+ triggerSubmit();
+ }
+
+ return
+ {hasWallets
+ ? <>
+ {/* Show the success/error alert if available */}
+ {alert}
+
+
+ {stakingForm}
+
+ {/* Send submit button */}
+
+ }
+ loading={isSubmitting}
+
+ // Prevent accidental space bar clicks
+ onKeyUp={e => e.preventDefault()}
+ >
+ {t("staking.buttonSubmit")}
+
+
+
+ {/* Clearfix for submit button floated right */}
+
+
+ >
+ : }
+ ;
+}
+
+function AlertSuccess({ stake }: { stake: TenebraStake }): JSX.Element {
+ const { t } = useTranslation();
+
+ return }
+ action={}
+ />;
+}
+
+function AlertError({ err }: { err: Error }): JSX.Element {
+ const { t } = useTranslation();
+
+ return ;
+}
diff --git a/src/pages/staking/Success.tsx b/src/pages/staking/Success.tsx
new file mode 100644
index 0000000..a6f7a17
--- /dev/null
+++ b/src/pages/staking/Success.tsx
@@ -0,0 +1,37 @@
+// 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 { Button } from "antd";
+
+import { useTranslation, Trans } from "react-i18next";
+
+import { Link } from "react-router-dom";
+
+import { TenebraStake } from "@api/types";
+import { TenebraValue } from "@comp/tenebra/TenebraValue";
+import { ContextualAddress } from "@comp/addresses/ContextualAddress";
+
+export function NotifSuccessContents({ stake }: { stake: TenebraStake }): JSX.Element {
+ const { t } = useTranslation();
+
+ return
+ Your new stae balance
+
+ after a staking action with
+
+ ;
+}
+
+export function NotifSuccessButton({ stake }: { stake: TenebraStake }): JSX.Element {
+ const { t } = useTranslation();
+
+ return
+
+ ;
+}
diff --git a/src/pages/staking/handleErrors.ts b/src/pages/staking/handleErrors.ts
new file mode 100644
index 0000000..8488ffc
--- /dev/null
+++ b/src/pages/staking/handleErrors.ts
@@ -0,0 +1,38 @@
+// 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 { TranslatedError } from "@utils/i18n";
+
+import { APIError } from "@api";
+import { ShowAuthFailedFn } from "@api/AuthFailed";
+
+import { Wallet } from "@wallets";
+
+export function handleStakingError(
+ onError: ((error: Error) => void) | undefined,
+ showAuthFailed: ShowAuthFailedFn,
+ err: Error,
+ from?: Wallet
+): void {
+ // Construct a TranslatedError pre-keyed to sendTransaction
+ const tErr = (key: string) => new TranslatedError("staking." + key);
+
+ switch (err.message) {
+ case "missing_parameter":
+ case "invalid_parameter":
+ switch ((err as APIError).parameter) {
+ case "privatekey":
+ return onError?.(tErr("errorParameterPrivatekey"));
+ case "amount":
+ return onError?.(tErr("errorParameterAmount"));
+ }
+ break;
+ case "insufficient_funds":
+ return onError?.(tErr("errorInsufficientFunds"));
+ case "auth_failed":
+ return showAuthFailed(from!);
+ }
+
+ // Pass through any other unknown errors
+ onError?.(err);
+}
diff --git a/src/tenebra/api/transactions.ts b/src/tenebra/api/transactions.ts
index 984cb82..c3d9133 100644
--- a/src/tenebra/api/transactions.ts
+++ b/src/tenebra/api/transactions.ts
@@ -3,7 +3,7 @@
// Full details: https://github.com/tmpim/TenebraWeb2/blob/master/LICENSE.txt
import { TranslatedError } from "@utils/i18n";
-import { TenebraTransaction } from "./types";
+import { TenebraStake, TenebraTransaction } from "./types";
import * as api from ".";
import { Wallet, decryptWallet } from "@wallets";
@@ -12,6 +12,10 @@
transaction: TenebraTransaction;
}
+interface StakingActionResponse {
+ stake: TenebraStake;
+}
+
export async function makeTransaction(
masterPassword: string,
from: Wallet,
@@ -35,3 +39,46 @@
return transaction;
}
+
+export async function makeDepositTransaction(
+ masterPassword: string,
+ from: Wallet,
+ amount: number,
+): Promise {
+ // Attempt to decrypt the wallet to get the privatekey
+ const decrypted = await decryptWallet(masterPassword, from);
+ if (!decrypted)
+ throw new TranslatedError("sendTransaction.errorWalletDecrypt");
+ const { privatekey } = decrypted;
+
+ const { stake } = await api.post(
+ "/staking",
+ {
+ privatekey, amount
+ }
+ );
+
+ return stake;
+}
+
+
+export async function makeWithdrawTransaction(
+ masterPassword: string,
+ from: Wallet,
+ amount: number,
+): Promise {
+ // Attempt to decrypt the wallet to get the privatekey
+ const decrypted = await decryptWallet(masterPassword, from);
+ if (!decrypted)
+ throw new TranslatedError("sendTransaction.errorWalletDecrypt");
+ const { privatekey } = decrypted;
+
+ const { stake } = await api.post(
+ "/staking/withdraw",
+ {
+ privatekey, amount
+ }
+ );
+
+ return stake;
+}
diff --git a/src/tenebra/api/types.ts b/src/tenebra/api/types.ts
index ba905ba..3fb2ad3 100644
--- a/src/tenebra/api/types.ts
+++ b/src/tenebra/api/types.ts
@@ -26,6 +26,12 @@
type: TenebraTransactionType;
}
+export interface TenebraStake {
+ owner: string;
+ stake: number;
+ active: boolean;
+}
+
export interface TenebraBlock {
height: number;
address: string;