diff --git a/apps/extension/src/Approvals/Approvals.tsx b/apps/extension/src/Approvals/Approvals.tsx index c39210bde..ec4ca67c7 100644 --- a/apps/extension/src/Approvals/Approvals.tsx +++ b/apps/extension/src/Approvals/Approvals.tsx @@ -3,6 +3,7 @@ import { ThemeProvider } from "styled-components"; import { Routes, Route } from "react-router-dom"; import { getTheme } from "@namada/utils"; +import { TxType } from "@namada/shared"; import { AppContainer, @@ -11,11 +12,11 @@ import { TopSection, Heading, } from "./Approvals.components"; -import { ApproveTransfer, ConfirmTransfer } from "./ApproveTransfer"; import { ApproveConnection } from "./ApproveConnection"; import { TopLevelRoute } from "Approvals/types"; -import { ConfirmLedgerTransfer } from "./ApproveTransfer/ConfirmLedgerTransfer"; -import { ApproveBond, ConfirmBond, ConfirmLedgerBond } from "./ApproveBond"; +import { ConfirmLedgerTx } from "./ApproveTx/ConfirmLedgerTx"; +import { ConfirmTx } from "./ApproveTx/ConfirmTx"; +import { ApproveTx } from "./ApproveTx/ApproveTx"; export enum Status { Completed, @@ -23,11 +24,17 @@ export enum Status { Failed, } +export type ApprovalDetails = { + source: string; + msgId: string; + txType: TxType; + publicKey?: string; + target?: string; +}; + export const Approvals: React.FC = () => { const theme = getTheme("dark"); - const [msgId, setMsgId] = useState(""); - const [address, setAddress] = useState(""); - const [publicKey, setPublicKey] = useState(""); + const [details, setDetails] = useState(); return ( @@ -39,44 +46,17 @@ export const Approvals: React.FC = () => { - } + path={`${TopLevelRoute.ApproveTx}/:type`} + element={} /> } + path={TopLevelRoute.ConfirmTx} + element={} /> } + path={TopLevelRoute.ConfirmLedgerTx} + element={} /> - - } - /> - } - /> - - } - /> - } diff --git a/apps/extension/src/Approvals/ApproveBond/ConfirmBond.tsx b/apps/extension/src/Approvals/ApproveBond/ConfirmBond.tsx deleted file mode 100644 index 88b44713e..000000000 --- a/apps/extension/src/Approvals/ApproveBond/ConfirmBond.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import { - Button, - ButtonVariant, - Input, - InputVariants, -} from "@namada/components"; -import { shortenAddress } from "@namada/utils"; - -import { Status } from "Approvals/Approvals"; -import { - ApprovalContainer, - ButtonContainer, - InfoHeader, - InfoLoader, -} from "Approvals/Approvals.components"; -import { Ports } from "router"; -import { useRequester } from "hooks/useRequester"; -import { SubmitApprovedBondMsg } from "background/approvals"; -import { Address } from "App/Accounts/AccountListing.components"; -import { closeCurrentTab } from "utils"; - -type Props = { - msgId: string; - address: string; -}; - -export const ConfirmBond: React.FC = ({ msgId, address }) => { - const navigate = useNavigate(); - const requester = useRequester(); - const [password, setPassword] = useState(""); - const [error, setError] = useState(); - const [status, setStatus] = useState(); - const [statusInfo, setStatusInfo] = useState(""); - - const handleApproveBond = async (): Promise => { - setStatus(Status.Pending); - try { - // TODO: use executeUntil here! - setStatusInfo("Decrypting keys and submitting transfer..."); - await requester.sendMessage( - Ports.Background, - new SubmitApprovedBondMsg(msgId, address, password) - ); - setStatus(Status.Completed); - } catch (e) { - setError("Unable to authenticate Tx!"); - setStatus(Status.Failed); - } - setStatusInfo(""); - setStatus(Status.Completed); - return; - }; - - useEffect(() => { - (async () => { - if (status === Status.Completed) { - await closeCurrentTab(); - } - })(); - }, [status]); - - return ( - - {status === Status.Pending && ( - - -

{statusInfo}

-
- )} - {status === Status.Failed && ( -

- {error} -
- Try again -

- )} - {status !== (Status.Pending || Status.Completed) && ( - <> -
- Decrypt keys for
{shortenAddress(address)}
-
- setPassword(e.target.value)} - /> - - - - - - )} -
- ); -}; diff --git a/apps/extension/src/Approvals/ApproveBond/index.ts b/apps/extension/src/Approvals/ApproveBond/index.ts deleted file mode 100644 index e25c4c155..000000000 --- a/apps/extension/src/Approvals/ApproveBond/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./ApproveBond"; -export * from "./ConfirmBond"; -export * from "./ConfirmLedgerBond"; diff --git a/apps/extension/src/Approvals/ApproveTransfer/ApproveTransfer.tsx b/apps/extension/src/Approvals/ApproveTransfer/ApproveTransfer.tsx deleted file mode 100644 index 792fe5492..000000000 --- a/apps/extension/src/Approvals/ApproveTransfer/ApproveTransfer.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useNavigate } from "react-router-dom"; -import { useEffect } from "react"; - -import { Button, ButtonVariant } from "@namada/components"; -import { shortenAddress } from "@namada/utils"; -import { AccountType, Tokens } from "@namada/types"; - -import { useQuery } from "hooks"; -import { Address } from "App/Accounts/AccountListing.components"; -import { - ApprovalContainer, - ButtonContainer, -} from "Approvals/Approvals.components"; -import { TopLevelRoute } from "Approvals/types"; -import { Ports } from "router"; -import { RejectTxMsg } from "background/approvals"; -import { useRequester } from "hooks/useRequester"; -import { closeCurrentTab } from "utils"; - -type Props = { - setMsgId: (msgId: string) => void; - setAddress: (address: string) => void; -}; - -export const ApproveTransfer: React.FC = ({ setAddress, setMsgId }) => { - const navigate = useNavigate(); - const requester = useRequester(); - - const query = useQuery(); - // TODO: Get current parent account alias to display to user - const type = query.get("type") || ""; - const id = query.get("id") || ""; - const amount = query.get("amount") || ""; - const source = query.get("source") || ""; - const target = query.get("target") || ""; - const tokenAddress = query.get("token") || ""; - const tokenType = - Object.values(Tokens).find((token) => token.address === tokenAddress) - ?.symbol || ""; - - useEffect(() => { - if (source) { - setAddress(source); - } - }, [source]); - - const handleApproveClick = (): void => { - setMsgId(id); - if (type === AccountType.Ledger) { - return navigate(`${TopLevelRoute.ConfirmLedgerTransfer}`); - } - navigate(TopLevelRoute.ConfirmTransfer); - }; - - const handleReject = async (): Promise => { - try { - // TODO: use executeUntil here! - await requester.sendMessage(Ports.Background, new RejectTxMsg(id)); - - // Close tab - await closeCurrentTab(); - } catch (e) { - console.warn(e); - } - return; - }; - - return ( - -

Approve this Transaction?

-

Target: 

-
{shortenAddress(target)}
-

Source: 

-
{shortenAddress(source)}
-

- Amount: {amount} {tokenType} -

- - - - - -
- ); -}; diff --git a/apps/extension/src/Approvals/ApproveTransfer/ConfirmLedgerTransfer.tsx b/apps/extension/src/Approvals/ApproveTransfer/ConfirmLedgerTransfer.tsx deleted file mode 100644 index 9877b3f30..000000000 --- a/apps/extension/src/Approvals/ApproveTransfer/ConfirmLedgerTransfer.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useCallback, useState } from "react"; -import { LedgerError } from "@namada/ledger-namada"; -import { toBase64 } from "@cosmjs/encoding"; - -import { Button, ButtonVariant } from "@namada/components"; - -import { Ledger } from "background/ledger"; -import { - GetTransferBytesMsg, - SubmitSignedTransferMsg, -} from "background/ledger/messages"; -import { Ports } from "router"; -import { closeCurrentTab } from "utils"; -import { useRequester } from "hooks/useRequester"; -import { Status } from "Approvals/Approvals"; -import { - ApprovalContainer, - ButtonContainer, -} from "Approvals/Approvals.components"; - -type Props = { - msgId: string; -}; - -export const ConfirmLedgerTransfer: React.FC = ({ msgId }) => { - const requester = useRequester(); - const [error, setError] = useState(); - const [status, setStatus] = useState(); - - const submitTransfer = async (): Promise => { - setStatus(Status.Pending); - const ledger = await Ledger.init(); - - try { - // Constuct tx bytes from SDK - const { bytes, path } = await requester.sendMessage( - Ports.Background, - new GetTransferBytesMsg(msgId) - ); - - // Sign with Ledger - const signatures = await ledger.sign(bytes, path); - const { errorMessage, returnCode } = signatures; - - // Close transport so that it may be re-opened on a subsequent attempt (due to error) - await ledger.closeTransport(); - - if (returnCode !== LedgerError.NoErrors) { - setError(errorMessage); - return setStatus(Status.Failed); - } - - // Submit signatures for tx - await requester.sendMessage( - Ports.Background, - new SubmitSignedTransferMsg(msgId, toBase64(bytes), signatures) - ); - setStatus(Status.Completed); - } catch (e) { - console.warn(e); - const ledgerErrors = await ledger.queryErrors(); - setError(ledgerErrors); - setStatus(Status.Failed); - } - }; - - const handleCloseTab = useCallback(async (): Promise => { - await closeCurrentTab(); - }, []); - - return ( - - {status === Status.Failed && ( -

- {error} -
- Try again -

- )} - {status === Status.Pending &&

Submitting transfer...

} - {status !== Status.Pending && status !== Status.Completed && ( - <> -

Make sure your Ledger is unlocked, and click "Submit"

- - - - - )} - {status === Status.Completed && ( - <> -

Success! You may close this window.

- - - - - )} -
- ); -}; diff --git a/apps/extension/src/Approvals/ApproveTransfer/ConfirmTransfer.tsx b/apps/extension/src/Approvals/ApproveTransfer/ConfirmTransfer.tsx deleted file mode 100644 index ca9c92de1..000000000 --- a/apps/extension/src/Approvals/ApproveTransfer/ConfirmTransfer.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import { Button, ButtonVariant, Input, InputVariants } from "@namada/components"; -import { shortenAddress } from "@namada/utils"; - -import { Status } from "Approvals/Approvals"; -import { - ApprovalContainer, - ButtonContainer, - InfoHeader, - InfoLoader, -} from "Approvals/Approvals.components"; -import { Ports } from "router"; -import { useRequester } from "hooks/useRequester"; -import { SubmitApprovedTransferMsg } from "background/approvals"; -import { Address } from "App/Accounts/AccountListing.components"; -import { closeCurrentTab } from "utils"; -import { FetchAndStoreMaspParamsMsg, HasMaspParamsMsg } from "provider"; - -type Props = { - msgId: string; - address: string; -}; - -export const ConfirmTransfer: React.FC = ({ msgId, address }) => { - const navigate = useNavigate(); - const requester = useRequester(); - const [password, setPassword] = useState(""); - const [error, setError] = useState(); - const [status, setStatus] = useState(); - const [statusInfo, setStatusInfo] = useState(""); - - const handleApproveTransfer = async (): Promise => { - setStatus(Status.Pending); - const hasMaspParams = await requester.sendMessage( - Ports.Background, - new HasMaspParamsMsg() - ); - - if (!hasMaspParams) { - setStatusInfo("Fetching MASP parameters..."); - try { - await requester.sendMessage( - Ports.Background, - new FetchAndStoreMaspParamsMsg() - ); - } catch (e) { - setError(`Fetching MASP parameters failed: ${e}`); - setStatus(Status.Failed); - } - } - try { - // TODO: use executeUntil here! - setStatusInfo("Decrypting keys and submitting transfer..."); - await requester.sendMessage( - Ports.Background, - new SubmitApprovedTransferMsg(msgId, address, password) - ); - setStatus(Status.Completed); - } catch (e) { - setError("Unable to authenticate Tx!"); - setStatus(Status.Failed); - } - setStatusInfo(""); - setStatus(Status.Completed); - return; - }; - - useEffect(() => { - (async () => { - if (status === Status.Completed) { - await closeCurrentTab(); - } - })(); - }, [status]); - - return ( - - {status === Status.Pending && ( - - -

{statusInfo}

-
- )} - {status === Status.Failed && ( -

- {error} -
- Try again -

- )} - {status !== (Status.Pending || Status.Completed) && ( - <> -
- Decrypt keys for
{shortenAddress(address)}
-
- setPassword(e.target.value)} - /> - - - - - - )} -
- ); -}; diff --git a/apps/extension/src/Approvals/ApproveTransfer/index.ts b/apps/extension/src/Approvals/ApproveTransfer/index.ts deleted file mode 100644 index 591a51410..000000000 --- a/apps/extension/src/Approvals/ApproveTransfer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./ApproveTransfer"; -export * from "./ConfirmTransfer"; -export * from "./ConfirmLedgerTransfer"; diff --git a/apps/extension/src/Approvals/ApproveBond/ApproveBond.tsx b/apps/extension/src/Approvals/ApproveTx/ApproveTx.tsx similarity index 62% rename from apps/extension/src/Approvals/ApproveBond/ApproveBond.tsx rename to apps/extension/src/Approvals/ApproveTx/ApproveTx.tsx index d7dd0c9e8..5890d7ea4 100644 --- a/apps/extension/src/Approvals/ApproveBond/ApproveBond.tsx +++ b/apps/extension/src/Approvals/ApproveTx/ApproveTx.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect } from "react"; import { Button, ButtonVariant } from "@namada/components"; import { shortenAddress } from "@namada/utils"; import { AccountType, Tokens } from "@namada/types"; +import { TxType } from "@namada/shared"; import { useQuery } from "hooks"; import { Address } from "App/Accounts/AccountListing.components"; @@ -11,31 +12,31 @@ import { ApprovalContainer, ButtonContainer, } from "Approvals/Approvals.components"; -import { TopLevelRoute } from "Approvals/types"; +import { TopLevelRoute, TxTypeLabel } from "Approvals/types"; import { Ports } from "router"; import { RejectTxMsg } from "background/approvals"; import { useRequester } from "hooks/useRequester"; import { closeCurrentTab } from "utils"; +import { useSanitizedParams } from "@namada/hooks"; +import { ApprovalDetails } from "Approvals/Approvals"; type Props = { - setAddress: (address: string) => void; - setMsgId: (msgId: string) => void; - setPublicKey: (publicKey: string) => void; + setDetails: (details: ApprovalDetails) => void; }; -export const ApproveBond: React.FC = ({ - setAddress, - setMsgId, - setPublicKey, -}) => { +export const ApproveTx: React.FC = ({ setDetails }) => { const navigate = useNavigate(); const requester = useRequester(); + const params = useSanitizedParams(); + const txType = parseInt(params?.type || "0"); + const query = useQuery(); - const type = query.get("type") || ""; - const id = query.get("id") || ""; + const accountType = query.get("accountType") || ""; + const msgId = query.get("id") || ""; const amount = query.get("amount") || ""; const source = query.get("source") || ""; + const target = query.get("target") || ""; const tokenAddress = query.get("token") || ""; const tokenType = Object.values(Tokens).find((token) => token.address === tokenAddress) @@ -43,26 +44,26 @@ export const ApproveBond: React.FC = ({ const publicKey = query.get("publicKey") || ""; useEffect(() => { - if (source) { - setAddress(source); - } - if (publicKey) { - setPublicKey(publicKey); - } - }, [source, publicKey]); + setDetails({ + source, + txType, + msgId, + publicKey, + target, + }); + }, [source, publicKey, txType, target, msgId]); const handleApproveClick = (): void => { - setMsgId(id); - if (type === AccountType.Ledger) { - return navigate(`${TopLevelRoute.ConfirmLedgerBond}`); + if (accountType === AccountType.Ledger) { + return navigate(`${TopLevelRoute.ConfirmLedgerTx}`); } - navigate(TopLevelRoute.ConfirmBond); + navigate(TopLevelRoute.ConfirmTx); }; const handleReject = useCallback(async (): Promise => { try { // TODO: use executeUntil here! - await requester.sendMessage(Ports.Background, new RejectTxMsg(id)); + await requester.sendMessage(Ports.Background, new RejectTxMsg(msgId)); // Close tab await closeCurrentTab(); @@ -70,13 +71,22 @@ export const ApproveBond: React.FC = ({ console.warn(e); } return; - }, [id]); + }, [msgId]); return ( -

Approve this Bond?

+

+ Approve this {TxTypeLabel[txType as TxType]} + transaction? +

Source: 

{shortenAddress(source)}
+ {target && ( + <> +

Target: 

+
{shortenAddress(target)}
+ + )}

Amount: {amount} {tokenType}

diff --git a/apps/extension/src/Approvals/ApproveBond/ConfirmLedgerBond.tsx b/apps/extension/src/Approvals/ApproveTx/ConfirmLedgerTx.tsx similarity index 63% rename from apps/extension/src/Approvals/ApproveBond/ConfirmLedgerBond.tsx rename to apps/extension/src/Approvals/ApproveTx/ConfirmLedgerTx.tsx index d1581f714..b431e95aa 100644 --- a/apps/extension/src/Approvals/ApproveBond/ConfirmLedgerBond.tsx +++ b/apps/extension/src/Approvals/ApproveTx/ConfirmLedgerTx.tsx @@ -2,49 +2,49 @@ import { useCallback, useState } from "react"; import { toBase64 } from "@cosmjs/encoding"; import BigNumber from "bignumber.js"; -import { LedgerError } from "@namada/ledger-namada"; +import { LedgerError, ResponseSign } from "@namada/ledger-namada"; import { Button, ButtonVariant } from "@namada/components"; import { defaultChainId as chainId } from "@namada/chains"; +import { TxType } from "@namada/shared"; +import { + Message, + RevealPKProps, + SubmitRevealPKMsgValue, + Tokens, +} from "@namada/types"; import { Ledger } from "background/ledger"; import { GetBondBytesMsg, + GetTransferBytesMsg, GetRevealPKBytesMsg, SubmitSignedBondMsg, SubmitSignedRevealPKMsg, + SubmitSignedTransferMsg, } from "background/ledger/messages"; import { Ports } from "router"; import { closeCurrentTab } from "utils"; import { useRequester } from "hooks/useRequester"; -import { Status } from "Approvals/Approvals"; +import { ApprovalDetails, Status } from "Approvals/Approvals"; import { ApprovalContainer, ButtonContainer, } from "Approvals/Approvals.components"; import { InfoHeader, InfoLoader } from "Approvals/Approvals.components"; -import { - Message, - RevealPKProps, - SubmitRevealPKMsgValue, - Tokens, -} from "@namada/types"; + import { QueryPublicKeyMsg } from "background/keyring"; +import { TxTypeLabel } from "Approvals/types"; type Props = { - address: string; - msgId: string; - publicKey: string; + details?: ApprovalDetails; }; -export const ConfirmLedgerBond: React.FC = ({ - address, - msgId, - publicKey, -}) => { +export const ConfirmLedgerTx: React.FC = ({ details }) => { const requester = useRequester(); const [error, setError] = useState(); const [status, setStatus] = useState(); const [statusInfo, setStatusInfo] = useState(""); + const { source, msgId = "", publicKey, txType } = details || {}; const revealPk = async (publicKey: string): Promise => { const revealPKArgs: RevealPKProps = { @@ -101,36 +101,81 @@ export const ConfirmLedgerBond: React.FC = ({ ); }; - const submitBond = async (): Promise => { + const handleSubmitTx = useCallback(async (): Promise => { setStatus(Status.Pending); setStatusInfo("Querying for public key on chain..."); - const pk = await queryPublicKey(address); + if (source && publicKey) { + const pk = await queryPublicKey(source); - if (!pk) { - setStatusInfo( - "Public key not found! Review and approve reveal pk on your Ledger" - ); - await revealPk(publicKey); + if (!pk) { + setStatusInfo( + "Public key not found! Review and approve reveal pk on your Ledger" + ); + await revealPk(publicKey); + } + + return await submitTx(); } + }, [source, publicKey]); + + const getBytesByType = async ( + type?: TxType + ): Promise<{ bytes: Uint8Array; path: string }> => { + switch (type) { + case TxType.Bond: + return await requester.sendMessage( + Ports.Background, + new GetBondBytesMsg(msgId) + ); + case TxType.Transfer: + return await requester.sendMessage( + Ports.Background, + new GetTransferBytesMsg(msgId) + ); + default: + throw new Error("Invalid transaction type!"); + } + }; + + // TODO: This will not be necessary when `submit_signed_tx` is implemented! + const submitByType = async ( + bytes: Uint8Array, + signatures: ResponseSign, + type?: TxType + ): Promise => { + switch (type) { + case TxType.Bond: + return await requester.sendMessage( + Ports.Background, + new SubmitSignedBondMsg(msgId, toBase64(bytes), signatures) + ); + case TxType.Transfer: + return await requester.sendMessage( + Ports.Background, + new SubmitSignedTransferMsg(msgId, toBase64(bytes), signatures) + ); + default: + throw new Error("Invalid transaction type!"); + } + }; + const submitTx = async (): Promise => { // Open ledger transport const ledger = await Ledger.init(); + const txLabel = TxTypeLabel[txType as TxType]; try { // Constuct tx bytes from SDK - const { bytes, path } = await requester.sendMessage( - Ports.Background, - new GetBondBytesMsg(msgId) - ); + const { bytes, path } = await getBytesByType(txType); + setStatusInfo(`Review and approve ${txLabel} transaction on your Ledger`); - setStatusInfo("Review and approve bond transaction on your Ledger"); // Sign with Ledger const signatures = await ledger.sign(bytes, path); const { errorMessage, returnCode } = signatures; if (returnCode !== LedgerError.NoErrors) { - console.warn("Bond sign errors encountered, exiting: ", { + console.warn(`${txLabel} signing errors encountered, exiting: `, { returnCode, errorMessage, }); @@ -139,11 +184,8 @@ export const ConfirmLedgerBond: React.FC = ({ } // Submit signatures for tx - setStatusInfo("Submitting bond transaction..."); - await requester.sendMessage( - Ports.Background, - new SubmitSignedBondMsg(msgId, toBase64(bytes), signatures) - ); + setStatusInfo(`Submitting ${txLabel} transaction...`); + await submitByType(bytes, signatures, txType); setStatus(Status.Completed); } catch (e) { console.warn(e); @@ -175,7 +217,7 @@ export const ConfirmLedgerBond: React.FC = ({ <>

Make sure your Ledger is unlocked, and click "Submit"

- diff --git a/apps/extension/src/Approvals/ApproveTx/ConfirmTx.tsx b/apps/extension/src/Approvals/ApproveTx/ConfirmTx.tsx new file mode 100644 index 000000000..811515e85 --- /dev/null +++ b/apps/extension/src/Approvals/ApproveTx/ConfirmTx.tsx @@ -0,0 +1,157 @@ +import { useCallback, useContext, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { + Button, + ButtonVariant, + Input, + InputVariants, +} from "@namada/components"; +import { shortenAddress } from "@namada/utils"; +import { TxType } from "@namada/shared"; + +import { ApprovalDetails, Status } from "Approvals/Approvals"; +import { + ApprovalContainer, + ButtonContainer, + InfoHeader, + InfoLoader, +} from "Approvals/Approvals.components"; +import { Ports } from "router"; +import { useRequester } from "hooks/useRequester"; +import { + SubmitApprovedBondMsg, + SubmitApprovedTransferMsg, + SubmitApprovedUnbondMsg, +} from "background/approvals"; +import { Address } from "App/Accounts/AccountListing.components"; +import { closeCurrentTab } from "utils"; +import { FetchAndStoreMaspParamsMsg, HasMaspParamsMsg } from "provider"; +import { TxTypeLabel } from "Approvals/types"; + +type Props = { + details?: ApprovalDetails; +}; + +export const ConfirmTx: React.FC = ({ details }) => { + const { source = "", msgId = "", txType } = details || {}; + const navigate = useNavigate(); + const requester = useRequester(); + const [password, setPassword] = useState(""); + const [error, setError] = useState(); + const [status, setStatus] = useState(); + const [statusInfo, setStatusInfo] = useState(""); + + const handleApproveTx = useCallback(async (): Promise => { + setStatus(Status.Pending); + setStatusInfo( + `Decrypting keys and submitting ${TxTypeLabel[txType as TxType]}...` + ); + + switch (txType) { + case TxType.Bond: { + await requester.sendMessage( + Ports.Background, + new SubmitApprovedBondMsg(msgId, password) + ); + setStatusInfo(""); + setStatus(Status.Completed); + break; + } + case TxType.Transfer: { + const hasMaspParams = await requester.sendMessage( + Ports.Background, + new HasMaspParamsMsg() + ); + + if (!hasMaspParams) { + setStatusInfo("Fetching MASP parameters..."); + try { + await requester.sendMessage( + Ports.Background, + new FetchAndStoreMaspParamsMsg() + ); + } catch (e) { + setError(`Fetching MASP parameters failed: ${e}`); + setStatus(Status.Failed); + } + } + try { + await requester.sendMessage( + Ports.Background, + new SubmitApprovedTransferMsg(msgId, password) + ); + setStatusInfo(""); + setStatus(Status.Completed); + } catch (e) { + console.info(e); + setError(`${e}`); + setStatus(Status.Failed); + } + break; + } + case TxType.Unbond: { + await requester.sendMessage( + Ports.Background, + new SubmitApprovedUnbondMsg(msgId, password) + ); + setStatusInfo(""); + setStatus(Status.Completed); + break; + } + } + }, [password]); + + useEffect(() => { + (async () => { + if (status === Status.Completed) { + await closeCurrentTab(); + } + })(); + }, [status]); + + return ( + + {status === Status.Pending && ( + + +

{statusInfo}

+
+ )} + {status === Status.Failed && ( +

+ {error} +
+ Try again +

+ )} + {status !== (Status.Pending || Status.Completed) && ( + <> +
+ Decrypt keys for
{shortenAddress(source)}
+
+ setPassword(e.target.value)} + /> + + + + + + )} +
+ ); +}; diff --git a/apps/extension/src/Approvals/ApproveTx/index.ts b/apps/extension/src/Approvals/ApproveTx/index.ts new file mode 100644 index 000000000..b5936e7f7 --- /dev/null +++ b/apps/extension/src/Approvals/ApproveTx/index.ts @@ -0,0 +1,3 @@ +export * from "./ApproveTx"; +export * from "./ConfirmTx"; +export * from "./ConfirmLedgerTx"; diff --git a/apps/extension/src/Approvals/types.ts b/apps/extension/src/Approvals/types.ts index 700fb022a..1c2313334 100644 --- a/apps/extension/src/Approvals/types.ts +++ b/apps/extension/src/Approvals/types.ts @@ -1,14 +1,21 @@ +import { TxType } from "@namada/shared"; + export enum TopLevelRoute { Default = "/", - ApproveConnection = "/approve-connection", - // Transfer - ApproveTransfer = "/approve-transfer", - ConfirmTransfer = "/confirm-transfer", - ConfirmLedgerTransfer = "/confirm-ledger-transfer", + // Connection approval + ApproveConnection = "/approve-connection", - // Bond - ApproveBond = "/approve-bond", - ConfirmBond = "/confirm-bond", - ConfirmLedgerBond = "/confirm-ledger-bond", + // Transaction approval + ApproveTx = "/approve-tx", + ConfirmTx = "/confirm-tx", + ConfirmLedgerTx = "/confirm-ledger-tx", } + +export const TxTypeLabel: Record = { + [TxType.Bond]: "bond", + [TxType.Unbond]: "unbond", + [TxType.Transfer]: "transfer", + [TxType.Withdraw]: "withdraw", + [TxType.RevealPK]: "reveal-pk", +}; diff --git a/apps/extension/src/background/approvals/handler.ts b/apps/extension/src/background/approvals/handler.ts index 653d92b81..3e8220881 100644 --- a/apps/extension/src/background/approvals/handler.ts +++ b/apps/extension/src/background/approvals/handler.ts @@ -71,8 +71,8 @@ const handleSubmitApprovedTransferMsg: ( const handleApproveBondMsg: ( service: ApprovalsService ) => InternalHandler = (service) => { - return async (_, { txMsg, accountType, publicKey }) => { - return await service.approveBond(txMsg, accountType, publicKey); + return async (_, { txMsg, accountType }) => { + return await service.approveBond(txMsg, accountType); }; }; diff --git a/apps/extension/src/background/approvals/messages.ts b/apps/extension/src/background/approvals/messages.ts index 43549e9b4..0f7e359a5 100644 --- a/apps/extension/src/background/approvals/messages.ts +++ b/apps/extension/src/background/approvals/messages.ts @@ -38,11 +38,7 @@ export class SubmitApprovedTransferMsg extends Message { return MessageType.SubmitApprovedTransfer; } - constructor( - public readonly msgId: string, - public readonly address: string, - public readonly password: string - ) { + constructor(public readonly msgId: string, public readonly password: string) { super(); } @@ -50,9 +46,6 @@ export class SubmitApprovedTransferMsg extends Message { if (!this.msgId) { throw new Error("msgId must not be empty!"); } - if (!this.address) { - throw new Error("address must not be empty!"); - } if (!this.password) { throw new Error( "Password is required to submitTx for this type of account!" @@ -76,11 +69,7 @@ export class SubmitApprovedBondMsg extends Message { return MessageType.SubmitApprovedBond; } - constructor( - public readonly msgId: string, - public readonly address: string, - public readonly password: string - ) { + constructor(public readonly msgId: string, public readonly password: string) { super(); } @@ -88,9 +77,6 @@ export class SubmitApprovedBondMsg extends Message { if (!this.msgId) { throw new Error("msgId must not be empty!"); } - if (!this.address) { - throw new Error("address must not be empty!"); - } if (!this.password) { throw new Error("Password is required to submit bond tx!"); } @@ -112,11 +98,7 @@ export class SubmitApprovedUnbondMsg extends Message { return MessageType.SubmitApprovedUnbond; } - constructor( - public readonly msgId: string, - public readonly address: string, - public readonly password: string - ) { + constructor(public readonly msgId: string, public readonly password: string) { super(); } @@ -124,9 +106,6 @@ export class SubmitApprovedUnbondMsg extends Message { if (!this.msgId) { throw new Error("msgId must not be empty!"); } - if (!this.address) { - throw new Error("address must not be empty!"); - } if (!this.password) { throw new Error("Password is required to submit unbond tx!"); } diff --git a/apps/extension/src/background/approvals/service.ts b/apps/extension/src/background/approvals/service.ts index 43d1050ca..b2fe3b9d1 100644 --- a/apps/extension/src/background/approvals/service.ts +++ b/apps/extension/src/background/approvals/service.ts @@ -9,6 +9,7 @@ import { SubmitBondMsgValue, TransferMsgValue, } from "@namada/types"; +import { TxType } from "@namada/shared"; import { KVStore } from "@namada/storage"; import { KeyRingService, TabStore } from "background/keyring"; @@ -21,7 +22,7 @@ export class ApprovalsService { protected readonly connectedTabsStore: KVStore, protected readonly keyRingService: KeyRingService, protected readonly ledgerService: LedgerService - ) { } + ) {} // Deserialize transfer details and prompt user async approveTransfer(txMsg: string, type?: AccountType): Promise { @@ -31,11 +32,17 @@ export class ApprovalsService { // Decode tx details and launch approval screen const txDetails = deserialize(txMsgBuffer, TransferMsgValue); - const { source, target, token, amount: amountBN } = txDetails; + const { + source, + target, + token, + amount: amountBN, + tx: { publicKey = "" }, + } = txDetails; const amount = new BigNumber(amountBN.toString()); - const baseUrl = `${browser.runtime.getURL( - "approvals.html" - )}#/approve-transfer`; + const baseUrl = `${browser.runtime.getURL("approvals.html")}#/approve-tx/${ + TxType.Transfer + }`; const url = paramsToUrl(baseUrl, { id, @@ -43,18 +50,15 @@ export class ApprovalsService { target, token, amount: amount.toString(), - type: type as string, + accountType: type as string, + publicKey, }); this._launchApprovalWindow(url); } // Deserialize bond details and prompt user - async approveBond( - txMsg: string, - type: AccountType, - publicKey?: string - ): Promise { + async approveBond(txMsg: string, type: AccountType): Promise { const txMsgBuffer = Buffer.from(fromBase64(txMsg)); const id = uuid(); await this.txStore.set(id, txMsg); @@ -62,17 +66,24 @@ export class ApprovalsService { // Decode tx details and launch approval screen const txDetails = deserialize(txMsgBuffer, SubmitBondMsgValue); - const { source, nativeToken: token, amount: amountBN } = txDetails; + const { + source, + nativeToken: token, + amount: amountBN, + tx: { publicKey = "" }, + } = txDetails; const amount = new BigNumber(amountBN.toString()); - const baseUrl = `${browser.runtime.getURL("approvals.html")}#/approve-bond`; + const baseUrl = `${browser.runtime.getURL("approvals.html")}#/approve-tx/${ + TxType.Bond + }`; const url = paramsToUrl(baseUrl, { id, source, token, amount: amount.toString(), - publicKey: publicKey || "", - type: type as string, + publicKey, + accountType: type as string, }); this._launchApprovalWindow(url); @@ -91,14 +102,16 @@ export class ApprovalsService { const { source, nativeToken, amount: amountBN } = txDetails; const amount = new BigNumber(amountBN.toString()); // TODO: This query should include perhaps a "type" indicating whether it's a bond or unbond tx: - const baseUrl = `${browser.runtime.getURL("approvals.html")}#/approve-bond`; + const baseUrl = `${browser.runtime.getURL("approvals.html")}#/approve-tx/${ + TxType.Unbond + }`; const url = paramsToUrl(baseUrl, { id, source, token: nativeToken, amount: amount.toString(), - type: type as string, + accountType: type as string, }); this._launchApprovalWindow(url); diff --git a/apps/extension/src/background/ledger/service.ts b/apps/extension/src/background/ledger/service.ts index ebb62fd82..1c3ac16ed 100644 --- a/apps/extension/src/background/ledger/service.ts +++ b/apps/extension/src/background/ledger/service.ts @@ -9,7 +9,7 @@ import { TransferMsgValue, } from "@namada/types"; import { ResponseSign } from "@namada/ledger-namada"; -import { Sdk } from "@namada/shared"; +import { Sdk, TxType } from "@namada/shared"; import { IStore, KVStore, Store } from "@namada/storage"; import { chains } from "@namada/chains"; import { makeBip44Path } from "@namada/utils"; @@ -62,7 +62,7 @@ export class LedgerService { throw new Error(`Ledger account not found for ${publicKey}`); } - const bytes = await this.sdk.build_reveal_pk(fromBase64(txMsg)); + const bytes = await this.sdk.build_tx(TxType.RevealPK, fromBase64(txMsg)); const path = makeBip44Path(coinType, account.path); return { bytes, path }; @@ -119,7 +119,7 @@ export class LedgerService { throw new Error(`Ledger account not found for ${source}`); } - const bytes = await this.sdk.build_transfer(fromBase64(txMsg)); + const bytes = await this.sdk.build_tx(TxType.Transfer, fromBase64(txMsg)); const path = makeBip44Path(coinType, account.path); return { bytes, path }; @@ -187,7 +187,7 @@ export class LedgerService { throw new Error(`Ledger account not found for ${source}`); } - const bytes = await this.sdk.build_bond(fromBase64(txMsg)); + const bytes = await this.sdk.build_tx(TxType.Bond, fromBase64(txMsg)); const path = makeBip44Path(coinType, account.path); return { bytes, path }; diff --git a/apps/extension/src/provider/Namada.ts b/apps/extension/src/provider/Namada.ts index 2222d6f87..123c5dc7b 100644 --- a/apps/extension/src/provider/Namada.ts +++ b/apps/extension/src/provider/Namada.ts @@ -27,7 +27,7 @@ export class Namada implements INamada { constructor( private readonly _version: string, protected readonly requester?: MessageRequester - ) { } + ) {} public async connect(chainId: string): Promise { return await this.requester?.sendMessage( @@ -94,10 +94,10 @@ export class Namada implements INamada { type: AccountType; publicKey?: string; }): Promise { - const { txMsg, type, publicKey } = props; + const { txMsg, type } = props; return await this.requester?.sendMessage( Ports.Background, - new ApproveBondMsg(txMsg, type, publicKey) + new ApproveBondMsg(txMsg, type) ); } diff --git a/apps/extension/src/provider/messages.ts b/apps/extension/src/provider/messages.ts index 43ce2f412..e295af2d0 100644 --- a/apps/extension/src/provider/messages.ts +++ b/apps/extension/src/provider/messages.ts @@ -295,8 +295,7 @@ export class ApproveBondMsg extends Message { constructor( public readonly txMsg: string, - public readonly accountType: AccountType, - public readonly publicKey?: string + public readonly accountType: AccountType ) { super(); } diff --git a/apps/namada-interface/.prettierrc b/apps/namada-interface/.prettierrc index f0eb61e0f..193626a11 100644 --- a/apps/namada-interface/.prettierrc +++ b/apps/namada-interface/.prettierrc @@ -2,5 +2,6 @@ "trailingComma": "es5", "tabWidth": 2, "semi": true, - "singleQuote": false + "singleQuote": false, + "bracketSpacing": false } diff --git a/apps/namada-interface/src/schema/index.ts b/apps/namada-interface/src/schema/index.ts deleted file mode 100644 index 6259ebc1b..000000000 --- a/apps/namada-interface/src/schema/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import BN from "bn.js"; - -export class TokenAmount { - micro: BN; - constructor(properties: { micro: BN }) { - this.micro = new BN(properties.micro); - } -} - -export const schemaAmount = new Map([ - [ - TokenAmount, - { - kind: "struct", - fields: [["micro", "u64"]], - }, - ], -]); diff --git a/apps/namada-interface/src/types/environment.d.ts b/apps/namada-interface/src/types/environment.d.ts index 36537b5a3..ec60be9bf 100644 --- a/apps/namada-interface/src/types/environment.d.ts +++ b/apps/namada-interface/src/types/environment.d.ts @@ -4,27 +4,18 @@ declare global { NODE_ENV: "development" | "production"; REACT_APP_LOCAL?: "true" | "false"; - // Default ledger - REACT_APP_LEDGER_URL?: string; - REACT_APP_LEDGER_PORT?: string; - REACT_APP_CHAIN_ID?: string; - REACT_APP_FAUCET?: string; + REACT_APP_NAMADA_ALIAS?: string; + REACT_APP_NAMADA_CHAIN_ID?: string; + REACT_APP_NAMADA_URL?: string; + REACT_APP_NAMADA_BECH32_PREFIX?: string; - // IBC Chain A - REACT_APP_CHAIN_A_ALIAS?: string; - REACT_APP_CHAIN_A_ID?: string; - REACT_APP_CHAIN_A_URL?: string; - REACT_APP_CHAIN_A_PORT?: string; - REACT_APP_CHAIN_A_FAUCET?: string; - REACT_APP_CHAIN_A_PORT_ID?: string; + REACT_APP_COSMOS_ALIAS?: string; + REACT_APP_COSMOS_CHAIN_ID?: string; + REACT_APP_COSMOS_CHAIN_URL?: string; - // IBC Chain B - REACT_APP_CHAIN_B_ALIAS?: string; - REACT_APP_CHAIN_B_ID?: string; - REACT_APP_CHAIN_B_URL?: string; - REACT_APP_CHAIN_B_PORT?: string; - REACT_APP_CHAIN_B_FAUCET?: string; - REACT_APP_CHAIN_B_PORT_ID?: string; + REACT_APP_OSMOSIS_ALIAS?: string; + REACT_APP_OSMOSIS_CHAIN_ID?: string; + REACT_APP_OSMOSIS_URL?: string; // CoinGecko REACT_APP_API_URL?: string; diff --git a/packages/shared/lib/src/sdk/mod.rs b/packages/shared/lib/src/sdk/mod.rs index 1004271ed..1cb61de3f 100644 --- a/packages/shared/lib/src/sdk/mod.rs +++ b/packages/shared/lib/src/sdk/mod.rs @@ -22,6 +22,16 @@ mod signature; mod tx; mod wallet; +#[wasm_bindgen] +#[derive(Copy, Clone, Debug)] +pub enum TxType { + Bond = 0, + Unbond = 1, + Withdraw = 2, + Transfer = 3, + RevealPK = 4, +} + // Require that a public key is present fn validate_pk(pk: Option) -> Result { match pk { @@ -197,48 +207,68 @@ impl Sdk { Ok(()) } - /// Contruct transfer data for external signers, returns byte array - pub async fn build_transfer(&mut self, tx_msg: &[u8]) -> Result { - let args = tx::transfer_tx_args(tx_msg, None, None)?; - - let transfer = namada::ledger::tx::build_transfer( - &self.client, - &mut self.wallet, - &mut self.shielded_ctx, - args.clone(), - ) - .await - .map_err(JsError::from)?; - - let bytes = transfer.0.try_to_vec().map_err(JsError::from)?; - - to_js_result(bytes) - } - - /// Contruct bond data for external signers, returns byte array - pub async fn build_bond(&mut self, tx_msg: &[u8]) -> Result { - let args = tx::bond_tx_args(tx_msg, None)?; - - let bond = namada::ledger::tx::build_bond(&self.client, &mut self.wallet, args.clone()) - .await - .map_err(JsError::from)?; - - let bytes = bond.0.try_to_vec().map_err(JsError::from)?; - - to_js_result(bytes) - } - - /// Contruct unbond data for external signers, returns byte array - pub async fn build_unbond(&mut self, tx_msg: &[u8]) -> Result { - let args = tx::unbond_tx_args(tx_msg, None)?; - - let unbond = namada::ledger::tx::build_unbond(&self.client, &mut self.wallet, args.clone()) - .await - .map_err(JsError::from)?; - - let bytes = unbond.0.try_to_vec().map_err(JsError::from)?; + /// Build transaction for specified type, return bytes to client + pub async fn build_tx(&mut self, tx_type: TxType, tx_msg: &[u8]) -> Result { + let tx = match tx_type { + TxType::Bond => { + let args = tx::bond_tx_args(tx_msg, None)?; + let bond = + namada::ledger::tx::build_bond(&self.client, &mut self.wallet, args.clone()) + .await + .map_err(JsError::from)?; + bond.0 + } + TxType::RevealPK => { + let args = tx::reveal_pk_tx_args(tx_msg)?; + + let reveal_pk = namada::ledger::tx::build_reveal_pk( + &self.client, + &mut self.wallet, + args::RevealPk { + tx: args.tx.clone(), + public_key: args.public_key.clone(), + }, + ) + .await?; + + match reveal_pk { + Some(v) => v.0, + None => { + return Err(JsError::new( + "Attempted to build reveal pk for existing public key!", + )) + } + } + } + TxType::Transfer => { + let args = tx::transfer_tx_args(tx_msg, None, None)?; + let transfer = namada::ledger::tx::build_transfer( + &self.client, + &mut self.wallet, + &mut self.shielded_ctx, + args.clone(), + ) + .await + .map_err(JsError::from)?; + transfer.0 + } + TxType::Unbond => { + let args = tx::unbond_tx_args(tx_msg, None)?; + let unbond = + namada::ledger::tx::build_unbond(&self.client, &mut self.wallet, args.clone()) + .await + .map_err(JsError::from)?; + unbond.0 + } + _ => { + return Err(JsError::new(&format!( + "TxType \"{:?}\" not implemented!", + tx_type, + ))) + } + }; - to_js_result(bytes) + to_js_result(tx.try_to_vec().map_err(JsError::from)?) } // Append signatures and return tx bytes @@ -284,6 +314,29 @@ impl Sdk { Ok(()) } + /// Submit signed tx + pub async fn submit_signed_tx( + &mut self, + tx_msg: &[u8], + tx_bytes: &[u8], + raw_sig_bytes: &[u8], + wrapper_sig_bytes: &[u8], + ) -> Result<(), JsError> { + let transfer_tx = self.sign_tx(tx_bytes, raw_sig_bytes, wrapper_sig_bytes)?; + let args = tx::tx_args_from_slice(tx_msg)?; + let verification_key = args.verification_key.clone(); + let pk = validate_pk(verification_key)?; + + self.submit_reveal_pk(&args, transfer_tx.clone(), &pk) + .await?; + + namada::ledger::tx::process_tx(&self.client, &mut self.wallet, &args, transfer_tx) + .await + .map_err(JsError::from)?; + + Ok(()) + } + pub async fn submit_transfer( &mut self, tx_msg: &[u8], diff --git a/packages/shared/lib/src/sdk/tx.rs b/packages/shared/lib/src/sdk/tx.rs index 522f5c265..515b16217 100644 --- a/packages/shared/lib/src/sdk/tx.rs +++ b/packages/shared/lib/src/sdk/tx.rs @@ -336,6 +336,13 @@ pub fn ibc_transfer_tx_args( Ok(args) } +pub fn tx_args_from_slice(tx_msg_bytes: &[u8]) -> Result { + let tx_msg = TxMsg::try_from_slice(tx_msg_bytes).map_err(JsError::from)?; + let args = tx_msg_into_args(tx_msg, None)?; + + Ok(args) +} + /// Maps serialized tx_msg into Tx args. /// This is common for all tx types. ///