From f26a160854abb5475c5d35dfb2dcb2d439959179 Mon Sep 17 00:00:00 2001 From: "Justin R. Evans" Date: Tue, 30 May 2023 12:21:13 +0200 Subject: [PATCH] Implement Ledger service with transfer functionality Minor updates - PR feedback Add typesafe pick function for filtering unwanted parameters in storage Make re-usable sign_and_process_tx function Fixing extension tests Fix Ledger balances after rebase Fix account derivation error Fix public key issue, update to latest 0.17.5 Add reveal pk tx Move reveal pk to reusable method Add reveal pk for submitting signed transfers --- .../src/App/Accounts/AccountListing.tsx | 22 +- apps/extension/src/App/App.tsx | 8 +- .../Settings/ExtraSettings/ExtraSettings.tsx | 49 ++- .../ResetPassword/ResetPassword.tsx | 49 ++- .../src/App/Settings/ExtraSettings/types.ts | 4 +- apps/extension/src/App/Settings/Settings.tsx | 79 ++--- apps/extension/src/Approvals/Approvals.tsx | 6 + .../ApproveTransfer/ApproveTransfer.tsx | 7 +- .../ApproveTransfer/ConfirmLedgerTransfer.tsx | 111 +++++++ .../ApproveTransfer/ConfirmTransfer.tsx | 1 + apps/extension/src/Approvals/types.ts | 1 + apps/extension/src/Setup/Ledger/Ledger.tsx | 111 +++---- .../src/Setup/Ledger/LedgerConfirmation.tsx | 89 ++++++ apps/extension/src/Setup/Setup.tsx | 13 +- apps/extension/src/Setup/types.ts | 1 + .../src/background/approvals/handler.ts | 4 +- .../src/background/approvals/messages.ts | 5 +- .../src/background/approvals/service.ts | 25 +- apps/extension/src/background/index.ts | 21 +- .../src/background/keyring/handler.ts | 6 +- .../src/background/keyring/keyring.ts | 161 ++++------ .../src/background/keyring/messages.ts | 22 +- .../src/background/keyring/service.ts | 71 ++++- .../extension/src/background/keyring/types.ts | 24 +- .../src/background/ledger/constants.ts | 1 + .../src/background/ledger/handler.ts | 58 ++++ apps/extension/src/background/ledger/index.ts | 3 + apps/extension/src/background/ledger/init.ts | 17 ++ .../extension/src/background/ledger/ledger.ts | 112 ++++--- .../src/background/ledger/messages.ts | 121 ++++++++ .../src/background/ledger/service.ts | 143 +++++++++ .../web-workers/submit-transfer-web-worker.ts | 4 +- apps/extension/src/provider/Anoma.test.ts | 21 +- apps/extension/src/provider/Anoma.ts | 15 +- apps/extension/src/provider/InjectedAnoma.ts | 20 +- apps/extension/src/provider/Signer.ts | 17 +- apps/extension/src/provider/data.mock.ts | 14 +- apps/extension/src/provider/messages.ts | 8 +- apps/extension/src/test/init.ts | 43 +-- apps/extension/src/utils/index.ts | 42 ++- apps/namada-interface/src/slices/accounts.ts | 5 +- apps/namada-interface/src/slices/transfers.ts | 26 +- apps/namada-interface/src/store/mocks.ts | 30 +- packages/integrations/src/Keplr.test.ts | 3 +- packages/integrations/src/Keplr.ts | 11 +- packages/integrations/src/Metamask.ts | 5 +- packages/shared/lib/Cargo.lock | 283 ++++++++++-------- packages/shared/lib/Cargo.toml | 6 - packages/shared/lib/src/sdk/mod.rs | 199 ++++++++++-- packages/shared/lib/src/sdk/tx.rs | 14 +- packages/types/src/account.ts | 9 +- packages/types/src/anoma.ts | 7 +- packages/types/src/signer.ts | 4 +- packages/types/src/tx/schema/index.ts | 1 - packages/types/src/tx/schema/shielded.ts | 86 ------ packages/types/src/tx/schema/tx.ts | 23 +- packages/types/src/tx/types.ts | 24 +- packages/utils/src/helpers/index.ts | 58 +++- 58 files changed, 1581 insertions(+), 742 deletions(-) create mode 100644 apps/extension/src/Approvals/ApproveTransfer/ConfirmLedgerTransfer.tsx create mode 100644 apps/extension/src/Setup/Ledger/LedgerConfirmation.tsx create mode 100644 apps/extension/src/background/ledger/constants.ts create mode 100644 apps/extension/src/background/ledger/handler.ts create mode 100644 apps/extension/src/background/ledger/init.ts create mode 100644 apps/extension/src/background/ledger/messages.ts create mode 100644 apps/extension/src/background/ledger/service.ts delete mode 100644 packages/types/src/tx/schema/shielded.ts diff --git a/apps/extension/src/App/Accounts/AccountListing.tsx b/apps/extension/src/App/Accounts/AccountListing.tsx index 6393fcd63c..002fc50cd9 100644 --- a/apps/extension/src/App/Accounts/AccountListing.tsx +++ b/apps/extension/src/App/Accounts/AccountListing.tsx @@ -29,6 +29,24 @@ const textToClipboard = (content: string): void => { navigator.clipboard.writeText(content); }; +const isChild = (type: AccountType, path: Bip44Path): boolean => { + // All PrivateKey accounts are child accounts + if (type === AccountType.PrivateKey) { + return true; + } + + if (type === AccountType.Ledger) { + // If this is a Ledger account, a child account is any account + // with a path that isn't the default path (/0'/0/0). This is for display + // purposes only. If the sum of the path components is greater than + // zero, it is a child. + const { account, change, index = 0 } = path; + return account + change + index > 0; + } + + return false; +}; + const formatDerivationPath = ( isChildAccount: boolean, { account, change, index = 0 }: Bip44Path, @@ -36,14 +54,14 @@ const formatDerivationPath = ( ): string => isChildAccount ? `/${account}'/${ - type === AccountType.PrivateKey ? `${change}/` : "" + type !== AccountType.Mnemonic ? `${change}/` : "" }${index}` : ""; const AccountListing = ({ account, parentAlias }: Props): JSX.Element => { const { address, alias, path, type } = account; const navigate = useNavigate(); - const isChildAccount = type !== AccountType.Mnemonic; + const isChildAccount = isChild(type, path); return ( diff --git a/apps/extension/src/App/App.tsx b/apps/extension/src/App/App.tsx index 5bd85db9f7..0927f98a1e 100644 --- a/apps/extension/src/App/App.tsx +++ b/apps/extension/src/App/App.tsx @@ -78,11 +78,13 @@ export const App: React.FC = () => { setStatus(Status.Pending); try { - const parentId = await requester.sendMessage( + const parent = await requester.sendMessage( Ports.Background, new GetActiveAccountMsg() ); - const parentAccount = accounts.find((account) => account.id === parentId); + const parentAccount = accounts.find( + (account) => account.id === parent?.id + ); setParentAccount(parentAccount); } catch (e) { console.error(e); @@ -115,7 +117,7 @@ export const App: React.FC = () => { }, onFail: () => { setError("An error occurred connecting to extension"); - setStatus(Status.Failed); + setStatus(Status.Completed); }, }, { tries: 10, ms: 100 }, diff --git a/apps/extension/src/App/Settings/ExtraSettings/ExtraSettings.tsx b/apps/extension/src/App/Settings/ExtraSettings/ExtraSettings.tsx index 51112e6555..6a64e7184b 100644 --- a/apps/extension/src/App/Settings/ExtraSettings/ExtraSettings.tsx +++ b/apps/extension/src/App/Settings/ExtraSettings/ExtraSettings.tsx @@ -4,10 +4,7 @@ import { ExtensionRequester } from "extension"; import { Mode, ExtraSetting } from "./types"; import { ResetPassword } from "./ResetPassword"; import { DeleteAccount } from "./DeleteAccount"; -import { - ExtraSettingsContainer, - CloseLink -} from "./ExtraSettings.components"; +import { ExtraSettingsContainer, CloseLink } from "./ExtraSettings.components"; /** * Container for additional settings forms such as the reset password form. @@ -17,37 +14,27 @@ const ExtraSettings: React.FC<{ requester: ExtensionRequester; onClose: () => void; onDeleteAccount: (id: string) => void; -}> = ({ - extraSetting, - requester, - onClose, - onDeleteAccount, -}) => { +}> = ({ extraSetting, requester, onClose, onDeleteAccount }) => { return ( - {extraSetting && - - Close - } - - { - extraSetting === null ? "" : - - extraSetting.mode === Mode.ResetPassword ? - : - - extraSetting.mode === Mode.DeleteAccount ? - : + {extraSetting && Close} + {extraSetting === null ? ( + "" + ) : extraSetting.mode === Mode.ResetPassword ? ( + + ) : extraSetting.mode === Mode.DeleteAccount ? ( + + ) : ( assertNever(extraSetting.mode) - } + )} ); }; diff --git a/apps/extension/src/App/Settings/ExtraSettings/ResetPassword/ResetPassword.tsx b/apps/extension/src/App/Settings/ExtraSettings/ResetPassword/ResetPassword.tsx index 666b4ed773..fce267e2c1 100644 --- a/apps/extension/src/App/Settings/ExtraSettings/ResetPassword/ResetPassword.tsx +++ b/apps/extension/src/App/Settings/ExtraSettings/ResetPassword/ResetPassword.tsx @@ -1,9 +1,6 @@ import { useState } from "react"; -import { - Button, - ButtonVariant -} from "@anoma/components"; -import zxcvbn, { ZXCVBNFeedback } from "zxcvbn"; +import { Button, ButtonVariant } from "@anoma/components"; +import zxcvbn from "zxcvbn"; import { Input, @@ -23,12 +20,12 @@ enum Status { Unsubmitted, Pending, Complete, - Failed -}; + Failed, +} export type Props = { - accountId: string, - requester: ExtensionRequester + accountId: string; + requester: ExtensionRequester; }; const ResetPassword: React.FC = ({ accountId, requester }) => { @@ -42,8 +39,8 @@ const ResetPassword: React.FC = ({ accountId, requester }) => { const match = newPassword === confirmNewPassword; const { feedback } = zxcvbn(newPassword); - const hasFeedback = feedback.warning !== "" || - feedback.suggestions.length > 0; + const hasFeedback = + feedback.warning !== "" || feedback.suggestions.length > 0; const shouldDisableSubmit = status === Status.Pending || @@ -52,7 +49,7 @@ const ResetPassword: React.FC = ({ accountId, requester }) => { !match || (process.env.NODE_ENV !== "development" && hasFeedback); - const handleSubmit = async () => { + const handleSubmit = async (): Promise => { setStatus(Status.Pending); const result = await requester.sendMessage( @@ -87,7 +84,7 @@ const ResetPassword: React.FC = ({ accountId, requester }) => { setCurrentPassword(e.target.value)} + onChange={(e) => setCurrentPassword(e.target.value)} /> @@ -96,16 +93,16 @@ const ResetPassword: React.FC = ({ accountId, requester }) => { setNewPassword(e.target.value)} + onChange={(e) => setNewPassword(e.target.value)} /> {feedback.warning} - {feedback.suggestions.map((suggestion, i) => + {feedback.suggestions.map((suggestion, i) => ( {suggestion} - )} + ))} @@ -113,35 +110,27 @@ const ResetPassword: React.FC = ({ accountId, requester }) => { setConfirmNewPassword(e.target.value)} + onChange={(e) => setConfirmNewPassword(e.target.value)} /> - {!match && ( - Passwords do not match - )} + {!match && Passwords do not match} - {errorMessage && ( - {errorMessage} - )} + {errorMessage && {errorMessage}} )} - {status === Status.Complete && ( -

Complete!

- )} + {status === Status.Complete &&

Complete!

} ); -} +}; export default ResetPassword; diff --git a/apps/extension/src/App/Settings/ExtraSettings/types.ts b/apps/extension/src/App/Settings/ExtraSettings/types.ts index a41b62d343..f7254a1263 100644 --- a/apps/extension/src/App/Settings/ExtraSettings/types.ts +++ b/apps/extension/src/App/Settings/ExtraSettings/types.ts @@ -2,9 +2,9 @@ export enum Mode { ResetPassword = "Reset password", DeleteAccount = "Delete account", -}; +} export type ExtraSetting = { mode: Mode; accountId: string; -} +}; diff --git a/apps/extension/src/App/Settings/Settings.tsx b/apps/extension/src/App/Settings/Settings.tsx index 5695cb8bac..d613350006 100644 --- a/apps/extension/src/App/Settings/Settings.tsx +++ b/apps/extension/src/App/Settings/Settings.tsx @@ -4,7 +4,6 @@ import browser from "webextension-polyfill"; import { DerivedAccount } from "@anoma/types"; import { Button, ButtonVariant } from "@anoma/components"; -import { assertNever } from "@anoma/utils"; import { ExtensionRequester } from "extension"; import { Ports } from "router"; @@ -12,6 +11,7 @@ import { LockKeyRingMsg, SetActiveAccountMsg, QueryParentAccountsMsg, + ParentAccount, } from "background/keyring"; import { SettingsContainer, @@ -29,10 +29,6 @@ import { } from "../Accounts/Accounts.components"; import { TopLevelRoute } from "../types"; import { Status } from "../App"; -import { - ResetPassword, - Props as ResetPasswordProps -} from "./ExtraSettings/ResetPassword" import { ExtraSettings } from "./ExtraSettings"; import { Mode, ExtraSetting } from "./ExtraSettings/types"; @@ -43,11 +39,7 @@ const Settings: React.FC<{ activeAccountId: string; requester: ExtensionRequester; onSelectAccount: (account: DerivedAccount) => void; -}> = ({ - activeAccountId, - requester, - onSelectAccount, -}) => { +}> = ({ activeAccountId, requester, onSelectAccount }) => { const [extraSetting, setExtraSetting] = useState(null); const [status, setStatus] = useState(Status.Pending); const [error, setError] = useState(""); @@ -75,19 +67,21 @@ const Settings: React.FC<{ fetchParentAccounts(); }, []); - const handleSelectAccount = async (account: DerivedAccount): Promise => { - const { id } = account; + const handleSelectAccount = async ( + account: DerivedAccount + ): Promise => { + const { id, type } = account; try { await requester.sendMessage( Ports.Background, - new SetActiveAccountMsg(id) + new SetActiveAccountMsg(id, type as ParentAccount) ); // Lock current wallet keyring: await requester.sendMessage(Ports.Background, new LockKeyRingMsg()); // Fetch accounts for selected parent account - await onSelectAccount(account); + onSelectAccount(account); } catch (e) { console.error(e); setError(`An error occurred while setting active account: ${e}`); @@ -95,7 +89,9 @@ const Settings: React.FC<{ } }; - const handleDeleteAccount = async (deletedAccountId: string): Promise => { + const handleDeleteAccount = async ( + deletedAccountId: string + ): Promise => { await fetchParentAccounts(); }; @@ -122,25 +118,26 @@ const Settings: React.FC<{ - {status === Status.Failed && (

Error communicating with extension background!

)}

{error ? error : "Select account:"}

- {parentAccounts.map((account, i) => + {parentAccounts.map((account, i) => ( handleSelectAccount(account)} - onSelectMode={(mode) => setExtraSetting({ - mode, - accountId: account.id - })} + onSelectMode={(mode) => + setExtraSetting({ + mode, + accountId: account.id, + }) + } /> - )} + ))} @@ -168,7 +165,6 @@ const Settings: React.FC<{ onClose={() => setExtraSetting(null)} onDeleteAccount={handleDeleteAccount} /> -
@@ -183,37 +179,28 @@ const AccountListItem: React.FC<{ activeAccountId: string; onSelectAccount: () => void; onSelectMode: (mode: Mode) => void; -}> = ({ - account, - activeAccountId, - onSelectAccount, - onSelectMode, -}) => { +}> = ({ account, activeAccountId, onSelectAccount, onSelectMode }) => { const [expanded, setExpanded] = useState(false); return ( <> - + {account.alias} {activeAccountId === account.id && (selected)} - setExpanded(!expanded)} - /> + setExpanded(!expanded)} /> - {expanded && + {expanded && ( { setExpanded(false); onSelectMode(mode); }} /> - } + )} ); }; @@ -223,24 +210,16 @@ const AccountListItem: React.FC<{ */ const ModeSelect: React.FC<{ onSelectMode: (mode: Mode) => void; -}> = ({ - onSelectMode, -}) => { - const modes = [ - Mode.ResetPassword, - Mode.DeleteAccount, - ]; +}> = ({ onSelectMode }) => { + const modes = [Mode.ResetPassword, Mode.DeleteAccount]; return ( - {modes.map((mode, i) => - onSelectMode(mode)} - > + {modes.map((mode, i) => ( + onSelectMode(mode)}> {mode} - )} + ))} ); }; diff --git a/apps/extension/src/Approvals/Approvals.tsx b/apps/extension/src/Approvals/Approvals.tsx index 979b3d0efe..b7db641a81 100644 --- a/apps/extension/src/Approvals/Approvals.tsx +++ b/apps/extension/src/Approvals/Approvals.tsx @@ -14,6 +14,7 @@ import { import { ApproveTransfer, ConfirmTransfer } from "./ApproveTransfer"; import { ApproveConnection } from "./ApproveConnection"; import { TopLevelRoute } from "Approvals/types"; +import { ConfirmLedgerTransfer } from "./ApproveTransfer/ConfirmLedgerTransfer"; export enum Status { Completed, @@ -45,6 +46,11 @@ export const Approvals: React.FC = () => { path={TopLevelRoute.ConfirmTx} element={} /> + } + /> + } diff --git a/apps/extension/src/Approvals/ApproveTransfer/ApproveTransfer.tsx b/apps/extension/src/Approvals/ApproveTransfer/ApproveTransfer.tsx index a68f33d073..a996cedbd9 100644 --- a/apps/extension/src/Approvals/ApproveTransfer/ApproveTransfer.tsx +++ b/apps/extension/src/Approvals/ApproveTransfer/ApproveTransfer.tsx @@ -3,7 +3,7 @@ import { useEffect } from "react"; import { Button, ButtonVariant } from "@anoma/components"; import { shortenAddress } from "@anoma/utils"; -import { Tokens } from "@anoma/types"; +import { AccountType, Tokens } from "@anoma/types"; import { useQuery } from "hooks"; import { Address } from "App/Accounts/AccountListing.components"; @@ -28,6 +28,7 @@ export const ApproveTransfer: React.FC = ({ setAddress, setMsgId }) => { 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") || ""; @@ -45,6 +46,9 @@ export const ApproveTransfer: React.FC = ({ setAddress, setMsgId }) => { const handleApproveClick = (): void => { setMsgId(id); + if (type === AccountType.Ledger) { + return navigate(`${TopLevelRoute.ConfirmLedgerTx}`); + } navigate(TopLevelRoute.ConfirmTx); }; @@ -64,7 +68,6 @@ export const ApproveTransfer: React.FC = ({ setAddress, setMsgId }) => { return (

Approve this Transaction?

-

ID: {id}

Target: 

{shortenAddress(target)}

Source: 

diff --git a/apps/extension/src/Approvals/ApproveTransfer/ConfirmLedgerTransfer.tsx b/apps/extension/src/Approvals/ApproveTransfer/ConfirmLedgerTransfer.tsx new file mode 100644 index 0000000000..e68dcd451e --- /dev/null +++ b/apps/extension/src/Approvals/ApproveTransfer/ConfirmLedgerTransfer.tsx @@ -0,0 +1,111 @@ +import { useCallback, useState } from "react"; +import { LedgerError } from "@zondax/ledger-namada"; +import { toBase64 } from "@cosmjs/encoding"; + +import { Button, ButtonVariant } from "@anoma/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) + ); + + // Retrieve publicKey from Ledger + const { publicKey } = await ledger.getAddressAndPublicKey(path); + + // 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, + publicKey + ) + ); + 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 index 7003bd54e9..49d39dc121 100644 --- a/apps/extension/src/Approvals/ApproveTransfer/ConfirmTransfer.tsx +++ b/apps/extension/src/Approvals/ApproveTransfer/ConfirmTransfer.tsx @@ -56,6 +56,7 @@ export const ConfirmTransfer: React.FC = ({ msgId, address }) => { Ports.Background, new SubmitApprovedTransferMsg(msgId, address, password) ); + setStatus(Status.Completed); } catch (e) { setError("Unable to authenticate Tx!"); setStatus(Status.Failed); diff --git a/apps/extension/src/Approvals/types.ts b/apps/extension/src/Approvals/types.ts index 1904b691e6..c771fcd2e4 100644 --- a/apps/extension/src/Approvals/types.ts +++ b/apps/extension/src/Approvals/types.ts @@ -3,4 +3,5 @@ export enum TopLevelRoute { ApproveConnection = "/connection", ApproveTx = "/tx", ConfirmTx = "/confirm", + ConfirmLedgerTx = "/confirm-ledger", } diff --git a/apps/extension/src/Setup/Ledger/Ledger.tsx b/apps/extension/src/Setup/Ledger/Ledger.tsx index df31345289..498602c197 100644 --- a/apps/extension/src/Setup/Ledger/Ledger.tsx +++ b/apps/extension/src/Setup/Ledger/Ledger.tsx @@ -11,10 +11,8 @@ import { Input, InputVariants, } from "@anoma/components"; -import { shortenAddress } from "@anoma/utils"; import { initLedgerHIDTransport, Ledger as LedgerApp } from "background/ledger"; -import { ExtensionRequester } from "extension"; import { LedgerError } from "./Ledger.components"; import { TopSection, @@ -24,31 +22,37 @@ import { SubViewContainer, UpperContentContainer, Header1, - BodyText, FormContainer, } from "Setup/Setup.components"; +import { TopLevelRoute } from "Setup/types"; -type Props = { - requester: ExtensionRequester; -}; - -const Ledger: React.FC = ({ requester: _ }) => { +const Ledger: React.FC = () => { const navigate = useNavigate(); const themeContext = useContext(ThemeContext); const [alias, setAlias] = useState(""); const [error, setError] = useState(); - const [isConnected, setIsConnected] = useState(false); - const [publicKey, setPublicKey] = useState(); - const [appInfo, setAppInfo] = useState<{ name: string; version: string }>(); const queryLedger = async (ledger: LedgerApp): Promise => { - const pk = await ledger.getPublicKey(); - const { appName, appVersion } = ledger.status(); + try { + // Get address and public key for default account + const { address, publicKey } = await ledger.getAddressAndPublicKey(); + navigate( + `/${TopLevelRoute.LedgerConfirmation}/${alias}/${address}/${publicKey}` + ); + } catch (_) { + checkErrors(ledger); + } + }; + + const checkErrors = async (ledger: LedgerApp): Promise => { + const errorMessage = await ledger.queryErrors(); - setAppInfo({ name: appName, version: appVersion }); - setPublicKey(pk); - setIsConnected(true); + if (errorMessage) { + await ledger.closeTransport(); + setError(errorMessage); + throw new Error(errorMessage); + } }; /** @@ -56,7 +60,8 @@ const Ledger: React.FC = ({ requester: _ }) => { */ const handleConnectUSB = async (): Promise => { try { - queryLedger(await LedgerApp.init()); + const ledger = await LedgerApp.init(); + queryLedger(ledger); } catch (e) { setError(`Failed to connect to Ledger: ${e}`); } @@ -68,7 +73,8 @@ const Ledger: React.FC = ({ requester: _ }) => { const handleConnectHID = async (): Promise => { try { const transport = await initLedgerHIDTransport(); - queryLedger(await LedgerApp.init(transport)); + const ledger = await LedgerApp.init(transport); + queryLedger(ledger); } catch (e) { setError(`Failed to connect to Ledger: ${e}`); } @@ -94,51 +100,30 @@ const Ledger: React.FC = ({ requester: _ }) => { {error && {error}} - {/* TODO: Navigate to next step for adding this account to background service. The following is temporary: */} - {isConnected && ( - <> - - Connection successful for "{alias}"! - - - Public key: {publicKey && shortenAddress(publicKey)} - - {appInfo && ( - <> - Name: {appInfo.name} - Version: {appInfo.version} - - )} - - )} - {!isConnected && ( - <> - - setAlias(e.target.value)} - variant={InputVariants.Text} - /> - - - - - - - )} + + setAlias(e.target.value)} + variant={InputVariants.Text} + /> + + + + + ); }; diff --git a/apps/extension/src/Setup/Ledger/LedgerConfirmation.tsx b/apps/extension/src/Setup/Ledger/LedgerConfirmation.tsx new file mode 100644 index 0000000000..d2291413f5 --- /dev/null +++ b/apps/extension/src/Setup/Ledger/LedgerConfirmation.tsx @@ -0,0 +1,89 @@ +import React, { useCallback, useContext, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { ThemeContext } from "styled-components"; + +import { + Button, + ButtonVariant, + Icon, + IconName, + IconSize, +} from "@anoma/components"; +import { shortenAddress } from "@anoma/utils"; +import { useSanitizedParams } from "@anoma/hooks"; + +import { useRequester } from "hooks/useRequester"; +import { LedgerError } from "./Ledger.components"; +import { + TopSection, + TopSectionHeaderContainer, + TopSectionButtonContainer, + ButtonsContainer, + SubViewContainer, + UpperContentContainer, + Header1, + BodyText, +} from "Setup/Setup.components"; +import { Ports } from "router"; +import { AddLedgerAccountMsg } from "background/ledger/messages"; +import { closeCurrentTab } from "utils"; + +const LedgerConfirmation: React.FC = () => { + const navigate = useNavigate(); + const requester = useRequester(); + const themeContext = useContext(ThemeContext); + const { alias = "", address = "", publicKey = "" } = useSanitizedParams(); + const [error, setError] = useState(); + + const handleSubmitClick = useCallback(async (): Promise => { + try { + await requester.sendMessage( + Ports.Background, + new AddLedgerAccountMsg(alias, address, publicKey, { + account: 0, + change: 0, + index: 0, + }) + ); + closeCurrentTab(); + } catch (e) { + console.warn(e); + setError(`${e}`); + } + }, []); + + return ( + + + + navigate(-1)} style={{ cursor: "pointer" }}> + + + + + + + + Confirm Ledger Connection + + + {error && {error}} + + Connection successful for "{alias}"! + + Address: {address && shortenAddress(address)} + Add this address to your wallet? + + + + + ); +}; + +export default LedgerConfirmation; diff --git a/apps/extension/src/Setup/Setup.tsx b/apps/extension/src/Setup/Setup.tsx index 39665e7a94..b6a80eb25b 100644 --- a/apps/extension/src/Setup/Setup.tsx +++ b/apps/extension/src/Setup/Setup.tsx @@ -28,6 +28,7 @@ import { ImportAccount } from "./ImportAccount"; import { useRequester } from "hooks/useRequester"; import { SeedPhraseImport } from "./ImportAccount/Steps"; import { Completion, Password } from "./Common"; +import LedgerConfirmation from "./Ledger/LedgerConfirmation"; type AnimatedTransitionProps = { elementKey: string; @@ -259,7 +260,17 @@ export const Setup: React.FC = () => { path={`/${TopLevelRoute.Ledger}`} element={ - + + + } + /> + + } /> diff --git a/apps/extension/src/Setup/types.ts b/apps/extension/src/Setup/types.ts index fc06a99d2e..ec902b93c1 100644 --- a/apps/extension/src/Setup/types.ts +++ b/apps/extension/src/Setup/types.ts @@ -3,6 +3,7 @@ export enum TopLevelRoute { AccountCreation = "account-creation", ImportAccount = "restore-account", Ledger = "ledger", + LedgerConfirmation = "ledger-confirmation", } export enum AccountCreationRoute { diff --git a/apps/extension/src/background/approvals/handler.ts b/apps/extension/src/background/approvals/handler.ts index b8fb368eac..151c526ddd 100644 --- a/apps/extension/src/background/approvals/handler.ts +++ b/apps/extension/src/background/approvals/handler.ts @@ -24,8 +24,8 @@ export const getHandler: (service: ApprovalsService) => Handler = (service) => { const handleApproveTxMsg: ( service: ApprovalsService ) => InternalHandler = (service) => { - return async (_, { txMsg }) => { - return await service.approveTransfer(txMsg); + return async (_, { txMsg, accountType }) => { + return await service.approveTransfer(txMsg, accountType); }; }; diff --git a/apps/extension/src/background/approvals/messages.ts b/apps/extension/src/background/approvals/messages.ts index a1ee99967e..e5572231ad 100644 --- a/apps/extension/src/background/approvals/messages.ts +++ b/apps/extension/src/background/approvals/messages.ts @@ -52,8 +52,11 @@ export class SubmitApprovedTransferMsg extends Message { throw new Error("address must not be empty!"); } if (!this.password) { - throw new Error("Password is required to submitTx!"); + throw new Error( + "Password is required to submitTx for this type of account!" + ); } + return; } diff --git a/apps/extension/src/background/approvals/service.ts b/apps/extension/src/background/approvals/service.ts index 0927155eb3..4098f5bde6 100644 --- a/apps/extension/src/background/approvals/service.ts +++ b/apps/extension/src/background/approvals/service.ts @@ -4,24 +4,26 @@ import { deserialize } from "borsh"; import { v4 as uuid } from "uuid"; import BigNumber from "bignumber.js"; -import { SubmitTransferMsgSchema, TransferMsgValue } from "@anoma/types"; +import { + AccountType, + SubmitTransferMsgSchema, + TransferMsgValue, +} from "@anoma/types"; import { KVStore } from "@anoma/storage"; -import { ExtensionRequester } from "extension"; -import { KeyRingService } from "background/keyring"; -import { TabStore } from "background/keyring"; +import { KeyRingService, TabStore } from "background/keyring"; +import { LedgerService } from "background/ledger"; export class ApprovalsService { constructor( protected readonly txStore: KVStore, protected readonly connectedTabsStore: KVStore, protected readonly keyRingService: KeyRingService, - protected readonly chainId: string, - protected readonly requester: ExtensionRequester + protected readonly ledgerService: LedgerService ) {} // Deserialize transfer details and prompt user - async approveTransfer(txMsg: string): Promise { + async approveTransfer(txMsg: string, type?: AccountType): Promise { const txMsgBuffer = Buffer.from(fromBase64(txMsg)); const id = uuid(); this.txStore.set(id, txMsg); @@ -36,7 +38,7 @@ export class ApprovalsService { const amount = new BigNumber(amountBN.toString()); const url = `${browser.runtime.getURL( "approvals.html" - )}#/tx?id=${id}&source=${source}&target=${target}&token=${token}&amount=${amount}`; + )}#/tx?type=${type}&id=${id}&source=${source}&target=${target}&token=${token}&amount=${amount}`; browser.windows.create({ url, @@ -53,20 +55,21 @@ export class ApprovalsService { await this._clearPendingTx(msgId); } - // Authenticate keyring, submit approved transaction from storage + // Authenticate keyring and submit approved transaction from storage async submitTransfer(msgId: string, password: string): Promise { - // TODO: Use executeUntil here: try { await this.keyRingService.unlock(password); } catch (e) { throw new Error(`${e}`); } + // Fetch pending tx const tx = await this.txStore.get(msgId); + // Validate account type if (tx) { await this.keyRingService.submitTransfer(tx, msgId); - // Clean up storage + return await this._clearPendingTx(msgId); } diff --git a/apps/extension/src/background/index.ts b/apps/extension/src/background/index.ts index 91c9980615..014f6d7fee 100644 --- a/apps/extension/src/background/index.ts +++ b/apps/extension/src/background/index.ts @@ -26,6 +26,7 @@ import { SDK_KEY, PARENT_ACCOUNT_ID_KEY, } from "./keyring"; +import { LedgerService, init as initLedger } from "./ledger"; const store = new IndexedDBKVStore(KVPrefix.IndexedDB); @@ -71,12 +72,12 @@ const { REACT_APP_NAMADA_URL = DEFAULT_URL } = process.env; const sdkData: Record | undefined = await sdkStore.get( SDK_KEY ); - const activeAccount = await activeAccountStore.get( - PARENT_ACCOUNT_ID_KEY - ); + const activeAccount = await activeAccountStore.get<{ + id: string; + }>(PARENT_ACCOUNT_ID_KEY); if (sdkData && activeAccount) { - const data = new TextEncoder().encode(sdkData[activeAccount]); + const data = new TextEncoder().encode(sdkData[activeAccount.id]); sdk.decode(data); } @@ -93,18 +94,26 @@ const { REACT_APP_NAMADA_URL = DEFAULT_URL } = process.env; cryptoMemory, requester ); + const ledgerService = new LedgerService( + keyRingService, + store, + connectedTabsStore, + txStore, + defaultChainId, + sdk + ); const approvalsService = new ApprovalsService( txStore, connectedTabsStore, keyRingService, - defaultChainId, - requester + ledgerService ); // Initialize messages and handlers initApprovals(router, approvalsService); initChains(router, chainsService); initKeyRing(router, keyRingService); + initLedger(router, ledgerService); router.listen(Ports.Background); })(); diff --git a/apps/extension/src/background/keyring/handler.ts b/apps/extension/src/background/keyring/handler.ts index 3705a68d1b..b33b52bd3a 100644 --- a/apps/extension/src/background/keyring/handler.ts +++ b/apps/extension/src/background/keyring/handler.ts @@ -283,8 +283,8 @@ const handleSetActiveAccountMsg: ( service: KeyRingService ) => InternalHandler = (service) => { return async (_, msg) => { - const { accountId } = msg; - return await service.setActiveAccountId(accountId); + const { accountId, accountType } = msg; + return await service.setActiveAccount(accountId, accountType); }; }; @@ -292,7 +292,7 @@ const handleGetActiveAccountMsg: ( service: KeyRingService ) => InternalHandler = (service) => { return async (_, _msg) => { - return await service.getActiveAccountId(); + return await service.getActiveAccount(); }; }; diff --git a/apps/extension/src/background/keyring/keyring.ts b/apps/extension/src/background/keyring/keyring.ts index 43a2ed1485..bfc3c35903 100644 --- a/apps/extension/src/background/keyring/keyring.ts +++ b/apps/extension/src/background/keyring/keyring.ts @@ -1,5 +1,6 @@ -import { v5 as uuid } from "uuid"; +import { deserialize } from "borsh"; +import { chains } from "@anoma/chains"; import { HDWallet, Mnemonic, @@ -24,39 +25,25 @@ import { SubmitTransferMsgSchema, TransferMsgValue, } from "@anoma/types"; -import { chains } from "@anoma/chains"; +import { + readVecStringPointer, + readStringPointer, +} from "@anoma/crypto/src/utils"; +import { makeBip44Path, Result } from "@anoma/utils"; + import { Crypto } from "./crypto"; import { + DeleteAccountError, + ActiveAccountStore, KeyRingStatus, KeyStore, ResetPasswordError, - DeleteAccountError, } from "./types"; -import { - readVecStringPointer, - readStringPointer, -} from "@anoma/crypto/src/utils"; -import { deserialize } from "borsh"; -import { Result } from "@anoma/utils"; +import { getAccountValuesFromStore, generateId } from "utils"; // Generated UUID namespace for uuid v5 const UUID_NAMESPACE = "9bfceade-37fe-11ed-acc0-a3da3461b38c"; -// Construct unique uuid, passing in an arbitray number of arguments. -// This could be a unique parameter of the object receiving the id, -// or an index based on the number of existing objects in a hierarchy. -const getId = (name: string, ...args: (number | string)[]): string => { - // Ensure a unique UUID - let uuidName = name; - - // Concatenate any number of args onto the name parameter - args.forEach((arg) => { - uuidName = `${uuidName}::${arg}`; - }); - - return uuid(uuidName, UUID_NAMESPACE); -}; - export const KEYSTORE_KEY = "key-store"; export const SDK_KEY = "sdk-store"; export const PARENT_ACCOUNT_ID_KEY = "parent-account-id"; @@ -82,7 +69,7 @@ export class KeyRing { constructor( protected readonly kvStore: KVStore, protected readonly sdkStore: KVStore>, - protected readonly activeAccountStore: KVStore, + protected readonly activeAccountStore: KVStore, protected readonly extensionStore: KVStore, protected readonly chainId: string, protected readonly sdk: Sdk, @@ -113,17 +100,19 @@ export class KeyRing { } } - public async getActiveAccountId(): Promise { + public async getActiveAccount(): Promise { return await this.activeAccountStore.get(PARENT_ACCOUNT_ID_KEY); } - public async setActiveAccountId(parentId: string): Promise { - await this.activeAccountStore.set(PARENT_ACCOUNT_ID_KEY, parentId); - + public async setActiveAccount( + id: string, + type: AccountType.Mnemonic | AccountType.Ledger + ): Promise { + await this.activeAccountStore.set(PARENT_ACCOUNT_ID_KEY, { id, type }); // To sync sdk wallet with DB const sdkData = await this.sdkStore.get(SDK_KEY); if (sdkData) { - this.sdk.decode(new TextEncoder().encode(sdkData[parentId])); + this.sdk.decode(new TextEncoder().encode(sdkData[id])); } } @@ -132,7 +121,7 @@ export class KeyRing { accountId?: string ): Promise { // default to active account if no account provided - const idToCheck = accountId ?? (await this.getActiveAccountId()); + const idToCheck = accountId ?? (await this.getActiveAccount())?.id; const mnemonic = await this._keyStore.getRecord("id", idToCheck); // TODO: Generate arbitrary data to check decryption against @@ -184,7 +173,7 @@ export class KeyRing { } // change password held locally if active account password changed - if (accountId === (await this.getActiveAccountId())) { + if (accountId === (await this.getActiveAccount())?.id) { this._password = newPassword; } } catch (error) { @@ -231,19 +220,24 @@ export class KeyRing { const mnemonic = Mnemonic.from_phrase(phrase); const seed = mnemonic.to_seed(); const { coinType } = chains[this.chainId].bip44; - const path = `m/44'/${coinType}'/0'/0`; - const bip44 = new HDWallet(seed); - const account = bip44.derive(path); + const path = { account: 0, change: 0 }; + const bip44Path = makeBip44Path(coinType, path); + const hdWallet = new HDWallet(seed); + const account = hdWallet.derive(bip44Path); const stringPointer = account.private().to_hex(); const sk = readStringPointer(stringPointer, this._cryptoMemory); const address = new Address(sk).implicit(); const { chainId } = this; // Generate unique ID for new parent account: - const id = getId(phrase, (await this._keyStore.get()).length); + const id = generateId( + UUID_NAMESPACE, + phrase, + (await this._keyStore.get()).length + ); mnemonic.free(); - bip44.free(); + hdWallet.free(); account.free(); stringPointer.free(); @@ -254,10 +248,7 @@ export class KeyRing { owner: address, chainId, password, - path: { - account: 0, - change: 0, - }, + path, text: phrase, type: AccountType.Mnemonic, }); @@ -267,7 +258,7 @@ export class KeyRing { // to prevent adding top level secret key to existing keys this.sdk.clear_storage(); await this.addSecretKey(sk, password, alias, id); - await this.setActiveAccountId(id); + await this.setActiveAccount(id, AccountType.Mnemonic); this._password = password; return true; @@ -282,26 +273,26 @@ export class KeyRing { path: Bip44Path, parentId: string ): DerivedAccountInfo { - const { account, change, index = 0 } = path; - const root = "m/44'"; const { coinType } = chains[this.chainId].bip44; - const derivationPath = [ - root, - `${coinType}'`, - `${account}`, - change, - index, - ].join("/"); - const bip44 = new HDWallet(seed); - const derivedAccount = bip44.derive(derivationPath); - + const derivationPath = makeBip44Path(coinType, path); + const hdWallet = new HDWallet(seed); + const derivedAccount = hdWallet.derive(derivationPath); const privateKey = derivedAccount.private(); const hex = privateKey.to_hex(); const text = readStringPointer(hex, this.cryptoMemory); const address = new Address(text).implicit(); - const id = getId("account", parentId, account, change, index); - bip44.free(); + const { account, change, index = 0 } = path; + const id = generateId( + UUID_NAMESPACE, + "account", + parentId, + account, + change, + index + ); + + hdWallet.free(); derivedAccount.free(); privateKey.free(); hex.free(); @@ -320,7 +311,7 @@ export class KeyRing { parentId: string ): DerivedAccountInfo { const { index = 0 } = path; - const id = getId("shielded-account", parentId, index); + const id = generateId("shielded-account", parentId, index); const zip32 = new ShieldedHDWallet(seed); const account = zip32.derive_to_serialized_keys(index); @@ -450,9 +441,10 @@ export class KeyRing { parentId: string; seed: VecU8Pointer; }> { + const activeAccount = await this.getActiveAccount(); const storedMnemonic = await this._keyStore.getRecord( "id", - await this.getActiveAccountId() + activeAccount?.id ); if (!storedMnemonic) { @@ -550,28 +542,18 @@ export class KeyRing { /** * Query accounts from storage (active parent account + associated derived child accounts) */ - public async queryAccounts(): Promise { - const activeAccountId = await this.getActiveAccountId(); + public async queryAccounts( + activeAccountId: string + ): Promise { const parentAccount = await this._keyStore.getRecord("id", activeAccountId); + const derivedAccounts = (await this._keyStore.getRecords("parentId", activeAccountId)) || []; if (parentAccount) { const accounts = [parentAccount, ...derivedAccounts]; - // Return only non-encrypted data - - return accounts.map( - ({ address, alias, chainId, path, parentId, id, type }) => ({ - address, - alias, - chainId, - id, - parentId, - path, - type, - }) - ); + return getAccountValuesFromStore(accounts); } return []; } @@ -580,22 +562,10 @@ export class KeyRing { * Query all top-level parent accounts (mnemonic accounts) */ public async queryParentAccounts(): Promise { - const accounts = await this._keyStore.getRecords( - "type", - AccountType.Mnemonic - ); + const accounts = + (await this._keyStore.getRecords("type", AccountType.Mnemonic)) || []; // Return only non-encrypted data - return (accounts || []).map( - ({ address, alias, chainId, path, parentId, id, type }) => ({ - address, - alias, - chainId, - id, - parentId, - path, - type, - }) - ); + return getAccountValuesFromStore(accounts); } async encodeInitAccount( @@ -713,7 +683,7 @@ export class KeyRing { } // remove password held locally if active account deleted - if (accountId === (await this.getActiveAccountId())) { + if (accountId === (await this.getActiveAccount())?.id) { this._password = undefined; } @@ -728,15 +698,10 @@ export class KeyRing { } async queryBalances( - address: string + owner: string ): Promise<{ token: string; amount: string }[]> { - const account = await this._keyStore.getRecord("address", address); - if (!account) { - throw new Error(`Account not found.`); - } - try { - return (await this.query.query_balance(account.owner)).map( + return (await this.query.query_balance(owner)).map( ([token, amount]: [string, string]) => { return { token, @@ -757,6 +722,10 @@ export class KeyRing { activeAccountId: string ): Promise { this.sdk.add_key(secretKey, password, alias); + await this.initSdkStore(activeAccountId); + } + + public async initSdkStore(activeAccountId: string): Promise { const store = (await this.sdkStore.get(SDK_KEY)) || {}; this.sdkStore.set(SDK_KEY, { diff --git a/apps/extension/src/background/keyring/messages.ts b/apps/extension/src/background/keyring/messages.ts index c0bd0ed407..7ddfb72788 100644 --- a/apps/extension/src/background/keyring/messages.ts +++ b/apps/extension/src/background/keyring/messages.ts @@ -2,7 +2,12 @@ import { PhraseSize } from "@anoma/crypto"; import { AccountType, Bip44Path, DerivedAccount } from "@anoma/types"; import { Message } from "router"; import { ROUTE } from "./constants"; -import { KeyRingStatus, ResetPasswordError, DeleteAccountError } from "./types"; +import { + KeyRingStatus, + ResetPasswordError, + DeleteAccountError, + ParentAccount, +} from "./types"; import { Result } from "@anoma/utils"; enum MessageType { @@ -237,7 +242,7 @@ export class ScanAccountsMsg extends Message { } // eslint-disable-next-line @typescript-eslint/no-empty-function - validate(): void {} + validate(): void { } route(): string { return ROUTE; @@ -294,7 +299,10 @@ export class SetActiveAccountMsg extends Message { return MessageType.SetActiveAccount; } - constructor(public readonly accountId: string) { + constructor( + public readonly accountId: string, + public readonly accountType: ParentAccount + ) { super(); } @@ -302,6 +310,10 @@ export class SetActiveAccountMsg extends Message { if (!this.accountId) { throw new Error("Account ID is not set!"); } + + if (!this.accountType) { + throw new Error("Account Type is required!"); + } } route(): string { @@ -313,7 +325,9 @@ export class SetActiveAccountMsg extends Message { } } -export class GetActiveAccountMsg extends Message { +export class GetActiveAccountMsg extends Message< + { id: string; type: ParentAccount } | undefined +> { public static type(): MessageType { return MessageType.GetActiveAccount; } diff --git a/apps/extension/src/background/keyring/service.ts b/apps/extension/src/background/keyring/service.ts index 558824546f..e7ece84ec7 100644 --- a/apps/extension/src/background/keyring/service.ts +++ b/apps/extension/src/background/keyring/service.ts @@ -1,18 +1,21 @@ import { fromBase64, toBase64 } from "@cosmjs/encoding"; import { PhraseSize } from "@anoma/crypto"; -import { KVStore } from "@anoma/storage"; +import { KVStore, Store } from "@anoma/storage"; import { AccountType, Bip44Path, DerivedAccount } from "@anoma/types"; import { Query, Sdk } from "@anoma/shared"; import { Result } from "@anoma/utils"; -import { KeyRing } from "./keyring"; +import { KeyRing, KEYSTORE_KEY } from "./keyring"; import { KeyRingStatus, KeyStore, TabStore, ResetPasswordError, DeleteAccountError, + ParentAccount, + ActiveAccountStore, + AccountStore, } from "./types"; import { syncTabs, updateTabStorage } from "./utils"; import { ExtensionRequester, getAnomaRouterId } from "extension"; @@ -30,14 +33,18 @@ import { SUBMIT_TRANSFER_MSG_TYPE, } from "background/offscreen"; import { init as initSubmitTransferWebWorker } from "background/web-workers"; +import { LEDGERSTORE_KEY } from "background/ledger"; +import { getAccountValuesFromStore } from "utils"; export class KeyRingService { private _keyRing: KeyRing; + private _keyRingStore: Store; + private _ledgerStore: Store; constructor( protected readonly kvStore: KVStore, protected readonly sdkStore: KVStore>, - protected readonly accountAccountStore: KVStore, + protected readonly activeAccountStore: KVStore, protected readonly connectedTabsStore: KVStore, protected readonly extensionStore: KVStore, protected readonly chainId: string, @@ -49,13 +56,15 @@ export class KeyRingService { this._keyRing = new KeyRing( kvStore, sdkStore, - accountAccountStore, + activeAccountStore, extensionStore, chainId, sdk, query, cryptoMemory ); + this._ledgerStore = new Store(LEDGERSTORE_KEY, kvStore); + this._keyRingStore = new Store(KEYSTORE_KEY, kvStore); } lock(): { status: KeyRingStatus } { @@ -145,11 +154,36 @@ export class KeyRingService { } async queryAccounts(): Promise { - return await this._keyRing.queryAccounts(); + const { id, type } = (await this.getActiveAccount()) || {}; + + if (type !== AccountType.Ledger && id) { + // Query KeyRing accounts + return await this._keyRing.queryAccounts(id); + } + + // Query Ledger accounts + const parent = await this._ledgerStore.getRecord("id", id); + + if (parent) { + const accounts = [ + parent, + ...((await this._ledgerStore.getRecords("parentId", id)) || []), + ]; + + return getAccountValuesFromStore(accounts); + } + + throw new Error(`No accounts found for ${id} ${type}`); } async queryParentAccounts(): Promise { - return await this._keyRing.queryParentAccounts(); + const ledgerAccounts = + (await this._ledgerStore.getRecords("parentId", undefined)) || []; + + return [ + ...(await this._keyRing.queryParentAccounts()), + ...getAccountValuesFromStore(ledgerAccounts), + ]; } async submitBond(txMsg: string): Promise { @@ -282,14 +316,13 @@ export class KeyRingService { return toBase64(tx_data); } - async setActiveAccountId(accountId: string): Promise { - await this._keyRing.setActiveAccountId(accountId); - + async setActiveAccount(id: string, type: ParentAccount): Promise { + await this._keyRing.setActiveAccount(id, type); await this.broadcastAccountsChanged(); } - async getActiveAccountId(): Promise { - return await this._keyRing.getActiveAccountId(); + async getActiveAccount(): Promise { + return await this._keyRing.getActiveAccount(); } async handleTransferCompleted( @@ -390,8 +423,20 @@ export class KeyRingService { } async queryBalances( - owner: string + address: string ): Promise<{ token: string; amount: string }[]> { - return this._keyRing.queryBalances(owner); + // Validate account + const account = + (await this._keyRingStore.getRecord("address", address)) || + (await this._ledgerStore.getRecord("address", address)); + + if (!account) { + throw new Error("Account not found!"); + } + return this._keyRing.queryBalances(account.owner); + } + + async initSdkStore(activeAccountId: string): Promise { + return await this._keyRing.initSdkStore(activeAccountId); } } diff --git a/apps/extension/src/background/keyring/types.ts b/apps/extension/src/background/keyring/types.ts index 4768c786e8..b132ca4adc 100644 --- a/apps/extension/src/background/keyring/types.ts +++ b/apps/extension/src/background/keyring/types.ts @@ -1,3 +1,4 @@ +import { StoredRecord } from "@anoma/storage"; import { AccountType, Bip44Path, DerivedAccount } from "@anoma/types"; export enum KdfType { @@ -21,12 +22,18 @@ export type ScryptParams = KdfParams & { p: number; }; -export interface KeyStore { - id: string; +export interface AccountStore extends StoredRecord { alias: string; address: string; owner: string; chainId: string; + publicKey?: string; + path: Bip44Path; + parentId?: string; + type: AccountType; +} + +export interface KeyStore extends AccountStore { crypto: { cipher: { type: "aes-256-gcm"; @@ -38,12 +45,6 @@ export interface KeyStore { params: T; }; }; - meta?: { - [key: string]: string; - }; - path: Bip44Path; - parentId?: string; - type: AccountType; } export type AccountState = DerivedAccount & { @@ -69,6 +70,13 @@ export enum ResetPasswordError { KeyStoreError, } +export type ParentAccount = AccountType.Mnemonic | AccountType.Ledger; + +export type ActiveAccountStore = { + id: string; + type: ParentAccount; +}; + export enum DeleteAccountError { BadPassword, KeyStoreError, diff --git a/apps/extension/src/background/ledger/constants.ts b/apps/extension/src/background/ledger/constants.ts new file mode 100644 index 0000000000..57854a916f --- /dev/null +++ b/apps/extension/src/background/ledger/constants.ts @@ -0,0 +1 @@ +export const ROUTE = "ledger-route"; diff --git a/apps/extension/src/background/ledger/handler.ts b/apps/extension/src/background/ledger/handler.ts new file mode 100644 index 0000000000..130157ba7f --- /dev/null +++ b/apps/extension/src/background/ledger/handler.ts @@ -0,0 +1,58 @@ +import { Handler, Env, Message, InternalHandler } from "router"; +import { LedgerService } from "./service"; +import { + AddLedgerAccountMsg, + GetTransferBytesMsg, + SubmitSignedTransferMsg, +} from "./messages"; + +export const getHandler: (service: LedgerService) => Handler = (service) => { + return (env: Env, msg: Message) => { + switch (msg.constructor) { + case AddLedgerAccountMsg: + return handleAddLedgerAccountMsg(service)( + env, + msg as AddLedgerAccountMsg + ); + case GetTransferBytesMsg: + return handleGetTransferBytesMsg(service)( + env, + msg as GetTransferBytesMsg + ); + case SubmitSignedTransferMsg: + return handleSubmitSignedTransferMsg(service)( + env, + msg as SubmitSignedTransferMsg + ); + default: + throw new Error("Unknown msg type"); + } + }; +}; + +const handleAddLedgerAccountMsg: ( + service: LedgerService +) => InternalHandler = (service) => { + return async (_, msg) => { + const { alias, address, publicKey, bip44Path } = msg; + return await service.addAccount(alias, address, publicKey, bip44Path); + }; +}; + +const handleGetTransferBytesMsg: ( + service: LedgerService +) => InternalHandler = (service) => { + return async (_, msg) => { + const { msgId } = msg; + return await service.getTransferBytes(msgId); + }; +}; + +const handleSubmitSignedTransferMsg: ( + service: LedgerService +) => InternalHandler = (service) => { + return async (_, msg) => { + const { bytes, publicKey, msgId, signatures } = msg; + return await service.submitTransfer(msgId, bytes, signatures, publicKey); + }; +}; diff --git a/apps/extension/src/background/ledger/index.ts b/apps/extension/src/background/ledger/index.ts index d1cd2cc47a..f2a87c7999 100644 --- a/apps/extension/src/background/ledger/index.ts +++ b/apps/extension/src/background/ledger/index.ts @@ -1 +1,4 @@ +export * from "./constants"; +export * from "./init"; export * from "./ledger"; +export * from "./service"; diff --git a/apps/extension/src/background/ledger/init.ts b/apps/extension/src/background/ledger/init.ts new file mode 100644 index 0000000000..7ca957544a --- /dev/null +++ b/apps/extension/src/background/ledger/init.ts @@ -0,0 +1,17 @@ +import { Router } from "router"; +import { ROUTE } from "./constants"; +import { + AddLedgerAccountMsg, + GetTransferBytesMsg, + SubmitSignedTransferMsg, +} from "./messages"; +import { getHandler } from "./handler"; +import { LedgerService } from "./service"; + +export function init(router: Router, service: LedgerService): void { + router.registerMessage(AddLedgerAccountMsg); + router.registerMessage(GetTransferBytesMsg); + router.registerMessage(SubmitSignedTransferMsg); + + router.addHandler(ROUTE, getHandler(service)); +} diff --git a/apps/extension/src/background/ledger/ledger.ts b/apps/extension/src/background/ledger/ledger.ts index 13fd0b08e9..a09965c5de 100644 --- a/apps/extension/src/background/ledger/ledger.ts +++ b/apps/extension/src/background/ledger/ledger.ts @@ -3,15 +3,17 @@ import { ResponseAppInfo, ResponseSign, ResponseVersion, + LedgerError, } from "@zondax/ledger-namada"; import TransportUSB from "@ledgerhq/hw-transport-webusb"; import TransportHID from "@ledgerhq/hw-transport-webhid"; import Transport from "@ledgerhq/hw-transport"; import { defaultChainId, chains } from "@anoma/chains"; +import { makeBip44Path } from "@anoma/utils"; const namadaChain = chains[defaultChainId]; -const bip44CoinType = namadaChain.bip44.coinType; +const { coinType } = namadaChain.bip44; export const initLedgerUSBTransport = async (): Promise => { return await TransportHID.create(); @@ -21,12 +23,13 @@ export const initLedgerHIDTransport = async (): Promise => { return await TransportUSB.create(); }; -export const DEFAULT_LEDGER_BIP44_PATH = `m/44'/${bip44CoinType}'/0'/0/0`; +export const DEFAULT_LEDGER_BIP44_PATH = makeBip44Path(coinType, { + account: 0, + change: 0, + index: 0, +}); export class Ledger { - public version: ResponseVersion | undefined; - public info: ResponseAppInfo | undefined; - constructor(public readonly namadaApp: NamadaApp | undefined = undefined) {} /** @@ -38,79 +41,92 @@ export class Ledger { const namadaApp = new NamadaApp(initializedTransport); const ledger = new Ledger(namadaApp); - ledger.version = await namadaApp.getVersion(); - ledger.info = await namadaApp.getAppInfo(); - return ledger; } /** * Return status and version info of initialized NamadaApp */ - public status(): { - appName: string; - appVersion: string; - errorMessage: string; - returnCode: number; - deviceLocked: boolean; - major: number; - minor: number; - patch: number; - targetId: string; - testMode: boolean; - } { - if (!this.version || !this.info) { + public async status(): Promise<{ + version: ResponseVersion; + info: ResponseAppInfo; + }> { + if (!this.namadaApp) { throw new Error("NamadaApp is not initialized!"); } - - const { appName, appVersion, errorMessage, returnCode } = this.info; - const { - major, - minor, - patch, - targetId, - deviceLocked = false, - testMode, - } = this.version; + const version = await this.namadaApp.getVersion(); + const info = await this.namadaApp.getAppInfo(); return { - appName, - appVersion, - errorMessage, - returnCode, - deviceLocked, - major, - minor, - patch, - targetId, - testMode, + version, + info, }; } /** - * Get public key associated with optional path, otherwise, use default path + * Get address and public key associated with optional path, otherwise, use default path */ - public async getPublicKey(bip44Path?: string): Promise { + public async getAddressAndPublicKey( + bip44Path?: string + ): Promise<{ address: string; publicKey: string }> { if (!this.namadaApp) { throw new Error("NamadaApp is not initialized!"); } const path = bip44Path || DEFAULT_LEDGER_BIP44_PATH; - const { publicKey } = await this.namadaApp.getAddressAndPubKey(path); + const { address, publicKey } = await this.namadaApp.getAddressAndPubKey( + path + ); - return publicKey.toString("hex"); + return { + // Return address as bech32-encoded string + address: address.toString(), + // Return public key as hex-encoded string + publicKey: publicKey.toString("hex"), + }; } /** * Sign tx bytes with the key associated with provided (or default) path + * + * @async + * @param {Uint8Array} tx - tx data blob to sign + * @param {string} bip44Path (optional) - Bip44 path for signing account + * + * @returns {ResponseSign} */ - public async sign(tx: ArrayBuffer, bip44Path: string): Promise { + public async sign(tx: Uint8Array, bip44Path?: string): Promise { if (!this.namadaApp) { throw new Error("NamadaApp is not initialized!"); } - + const buffer = Buffer.from(tx); const path = bip44Path || DEFAULT_LEDGER_BIP44_PATH; - return await this.namadaApp.sign(path, Buffer.from(tx)); + return await this.namadaApp.sign(path, buffer); + } + + /** + * Query status to determine if device has thrown an error + */ + public async queryErrors(): Promise { + const { + info: { returnCode, errorMessage }, + } = await this.status(); + + if (returnCode !== LedgerError.NoErrors) { + return errorMessage; + } + return ""; + } + + /** + * Close the initialized transport, which may be needed if Ledger needs to be reinitialized due to error state + */ + public async closeTransport(): Promise { + if (!this.namadaApp) { + throw new Error("NamadaApp is not initialized!"); + } + + return await this.namadaApp.transport.close(); } } diff --git a/apps/extension/src/background/ledger/messages.ts b/apps/extension/src/background/ledger/messages.ts new file mode 100644 index 0000000000..2d8ec228f4 --- /dev/null +++ b/apps/extension/src/background/ledger/messages.ts @@ -0,0 +1,121 @@ +import { ResponseSign } from "@zondax/ledger-namada"; + +import { Bip44Path } from "@anoma/types"; + +import { Message } from "router"; +import { ROUTE } from "./constants"; + +enum MessageType { + AddLedgerAccount = "add-ledger-account", + GetTransferBytes = "get-transfer-bytes", + SubmitSignedTransfer = "submit-signed-transfer", +} + +export class AddLedgerAccountMsg extends Message { + public static type(): MessageType { + return MessageType.AddLedgerAccount; + } + + constructor( + public readonly alias: string, + public readonly address: string, + public readonly publicKey: string, + public readonly bip44Path: Bip44Path + ) { + super(); + } + + validate(): void { + if (!this.alias) { + throw new Error("Alias must not be empty!"); + } + + if (!this.address) { + throw new Error("Address was not provided!"); + } + + if (!this.publicKey) { + throw new Error("Public key was not provided!"); + } + + if (!this.bip44Path) { + throw new Error("BIP44 Path was not provided!"); + } + } + + route(): string { + return ROUTE; + } + + type(): string { + return AddLedgerAccountMsg.type(); + } +} + +export class GetTransferBytesMsg extends Message<{ + bytes: Uint8Array; + path: string; +}> { + public static type(): MessageType { + return MessageType.GetTransferBytes; + } + + constructor(public readonly msgId: string) { + super(); + } + + validate(): void { + if (!this.msgId) { + throw new Error("msgId was not provided!"); + } + } + + route(): string { + return ROUTE; + } + + type(): string { + return GetTransferBytesMsg.type(); + } +} + +export class SubmitSignedTransferMsg extends Message { + public static type(): MessageType { + return MessageType.SubmitSignedTransfer; + } + + constructor( + public readonly msgId: string, + public readonly bytes: string, + public readonly signatures: ResponseSign, + public readonly publicKey: string + ) { + super(); + } + + validate(): void { + if (!this.msgId) { + throw new Error("msgId was not provided!"); + } + + if (!this.bytes) { + throw new Error("bytes were not provided!"); + } + + if (!this.signatures) { + throw new Error("No signatures were provided!"); + } + + if (!this.publicKey) { + throw new Error("No publicKey provided!"); + } + } + + route(): string { + return ROUTE; + } + + type(): string { + return SubmitSignedTransferMsg.type(); + } +} diff --git a/apps/extension/src/background/ledger/service.ts b/apps/extension/src/background/ledger/service.ts new file mode 100644 index 0000000000..92cbc10c5d --- /dev/null +++ b/apps/extension/src/background/ledger/service.ts @@ -0,0 +1,143 @@ +import { fromBase64, toHex } from "@cosmjs/encoding"; +import { deserialize } from "borsh"; +import { ResponseSign } from "@zondax/ledger-namada"; + +import { + AccountType, + Bip44Path, + SubmitTransferMsgSchema, + TransferMsgValue, +} from "@anoma/types"; +import { Sdk } from "@anoma/shared"; +import { IStore, KVStore, Store } from "@anoma/storage"; +import { chains } from "@anoma/chains"; +import { makeBip44Path } from "@anoma/utils"; + +import { AccountStore, KeyRingService, TabStore } from "background/keyring"; +import { generateId as generateId } from "utils"; + +export const LEDGERSTORE_KEY = "ledger-store"; +const UUID_NAMESPACE = "be9fdaee-ffa2-11ed-8ef1-325096b39f47"; + +export class LedgerService { + private _ledgerStore: IStore; + + constructor( + protected readonly keyring: KeyRingService, + protected readonly kvStore: KVStore, + protected readonly connectedTabsStore: KVStore, + protected readonly txStore: KVStore, + protected readonly chainId: string, + protected readonly sdk: Sdk + ) { + this._ledgerStore = new Store(LEDGERSTORE_KEY, kvStore); + } + + async getTransferBytes( + msgId: string + ): Promise<{ bytes: Uint8Array; path: string }> { + const txMsg = await this.txStore.get(msgId); + const { coinType } = chains[this.chainId].bip44; + + if (!txMsg) { + throw new Error(`Transaction ${msgId} not found!`); + } + + try { + // Deserialize txMsg to retrieve source + const { source } = deserialize( + SubmitTransferMsgSchema, + TransferMsgValue, + Buffer.from(fromBase64(txMsg)) + ); + + // Query account from Ledger storage to determine path for signer + const account = await this._ledgerStore.getRecord("address", source); + + if (!account) { + throw new Error(`Ledger account not found for ${source}`); + } + + const bytes = await this.sdk.build_transfer(fromBase64(txMsg)); + const path = makeBip44Path(coinType, account.path); + + return { bytes, path }; + } catch (e) { + console.warn(e); + throw new Error(`${e}`); + } + } + + async submitTransfer( + msgId: string, + bytes: string, + signatures: ResponseSign, + publicKey: string + ): Promise { + const txMsg = await this.txStore.get(msgId); + + if (!txMsg) { + throw new Error(`Transaction ${msgId} not found!`); + } + + const { headerSignature, codeSignature, dataSignature } = signatures; + + const signedTransfer = await this.sdk.sign_tx( + fromBase64(bytes), + toHex(dataSignature.signature), + toHex(codeSignature.signature), + toHex(headerSignature.signature) + ); + + try { + await this.sdk.submit_signed_transfer( + publicKey, + fromBase64(txMsg), + signedTransfer + ); + + // Clear pending tx if successful + await this.txStore.set(msgId, null); + } catch (e) { + console.warn(e); + } + } + + /** + * Append a new address record for use with Ledger + */ + async addAccount( + alias: string, + address: string, + publicKey: string, + bip44Path: Bip44Path + ): Promise { + // Check if account exists in storage, return if so: + const record = await this._ledgerStore.getRecord("address", address); + if (record) { + return; + } + + // Generate a UUID v5 unique id from alias & path + const id = generateId(UUID_NAMESPACE, alias, address); + + const account = { + id, + alias, + address, + publicKey, + owner: address, + chainId: this.chainId, + path: bip44Path, + type: AccountType.Ledger, + }; + await this._ledgerStore.append(account); + + // Prepare SDK store + this.sdk.clear_storage(); + await this.keyring.initSdkStore(id); + + // Set active account ID + await this.keyring.setActiveAccount(id, AccountType.Ledger); + } +} diff --git a/apps/extension/src/background/web-workers/submit-transfer-web-worker.ts b/apps/extension/src/background/web-workers/submit-transfer-web-worker.ts index 2a5b9f5b8a..9cca29bba7 100644 --- a/apps/extension/src/background/web-workers/submit-transfer-web-worker.ts +++ b/apps/extension/src/background/web-workers/submit-transfer-web-worker.ts @@ -22,12 +22,12 @@ import { const sdkData: Record | undefined = await sdkStore.get( "sdk-store" ); - const activeAccount = await activeAccountStore.get( + const activeAccount = await activeAccountStore.get<{ id: string }>( "parent-account-id" ); if (sdkData && activeAccount) { - const data = new TextEncoder().encode(sdkData[activeAccount]); + const data = new TextEncoder().encode(sdkData[activeAccount.id]); sdk.decode(data); } diff --git a/apps/extension/src/provider/Anoma.test.ts b/apps/extension/src/provider/Anoma.test.ts index a272a1a799..0922512e17 100644 --- a/apps/extension/src/provider/Anoma.test.ts +++ b/apps/extension/src/provider/Anoma.test.ts @@ -14,13 +14,15 @@ import { TransferProps, Chain, Anoma, + AccountType, } from "@anoma/types"; import { KVKeys } from "router"; import { init, KVStoreMock } from "test/init"; import { chains as defaultChains } from "@anoma/chains"; -import { chain, keyStore, password, ACTIVE_ACCOUNT_ID } from "./data.mock"; +import { chain, keyStore, password, ACTIVE_ACCOUNT } from "./data.mock"; import { + ActiveAccountStore, KeyRing, KeyRingService, KeyStore, @@ -36,7 +38,7 @@ jest.mock("webextension-polyfill", () => ({})); describe("Anoma", () => { let anoma: Anoma; let iDBStore: KVStoreMock; - let activeAccountStore: KVStoreMock; + let activeAccountStore: KVStoreMock; let keyRingService: KeyRingService; beforeAll(async () => { @@ -70,10 +72,12 @@ describe("Anoma", () => { it("should return all accounts", async () => { iDBStore.set(KEYSTORE_KEY, keyStore); - activeAccountStore.set(PARENT_ACCOUNT_ID_KEY, ACTIVE_ACCOUNT_ID); - const storedKeyStore = keyStore.map( - ({ crypto: _crypto, owner: _owner, ...account }) => account - ); + + activeAccountStore.set(PARENT_ACCOUNT_ID_KEY, { + id: ACTIVE_ACCOUNT.id, + type: AccountType.Mnemonic, + }); + const storedKeyStore = keyStore.map(({ crypto: _, ...account }) => account); const storedAccounts = await anoma.accounts(chain.chainId); expect(storedAccounts).toEqual(storedKeyStore); @@ -113,7 +117,10 @@ describe("Anoma", () => { ); jest.spyOn(keyRingService, "submitTransfer"); - anoma.submitTransfer(toBase64(serializedTransfer)); + anoma.submitTransfer({ + txMsg: toBase64(serializedTransfer), + type: AccountType.PrivateKey, + }); expect(keyRingService.submitTransfer).toBeCalled(); }); diff --git a/apps/extension/src/provider/Anoma.ts b/apps/extension/src/provider/Anoma.ts index 9599037f5a..07c041d503 100644 --- a/apps/extension/src/provider/Anoma.ts +++ b/apps/extension/src/provider/Anoma.ts @@ -1,4 +1,9 @@ -import { Anoma as IAnoma, Chain, DerivedAccount } from "@anoma/types"; +import { + AccountType, + Anoma as IAnoma, + Chain, + DerivedAccount, +} from "@anoma/types"; import { Ports, MessageRequester } from "router"; import { @@ -97,10 +102,14 @@ export class Anoma implements IAnoma { ); } - public async submitTransfer(txMsg: string): Promise { + public async submitTransfer(props: { + txMsg: string; + type?: AccountType; + }): Promise { + const { txMsg, type } = props; return await this.requester?.sendMessage( Ports.Background, - new ApproveTransferMsg(txMsg) + new ApproveTransferMsg(txMsg, type) ); } diff --git a/apps/extension/src/provider/InjectedAnoma.ts b/apps/extension/src/provider/InjectedAnoma.ts index 8b34a9a3dd..9bdf8ca883 100644 --- a/apps/extension/src/provider/InjectedAnoma.ts +++ b/apps/extension/src/provider/InjectedAnoma.ts @@ -1,4 +1,5 @@ import { + AccountType, Anoma as IAnoma, Chain, DerivedAccount, @@ -8,7 +9,7 @@ import { InjectedProxy } from "./InjectedProxy"; import { Signer } from "./Signer"; export class InjectedAnoma implements IAnoma { - constructor(private readonly _version: string) {} + constructor(private readonly _version: string) { } public async connect(chainId: string): Promise { return await InjectedProxy.requestMethod("connect", chainId); @@ -60,11 +61,18 @@ export class InjectedAnoma implements IAnoma { ); } - public async submitTransfer(txMsg: string): Promise { - return await InjectedProxy.requestMethod( - "submitTransfer", - txMsg - ); + public async submitTransfer(props: { + txMsg: string; + type: AccountType; + }): Promise { + const { txMsg, type } = props; + return await InjectedProxy.requestMethod< + { txMsg: string; type: AccountType }, + void + >("submitTransfer", { + txMsg, + type, + }); } public async submitIbcTransfer(txMsg: string): Promise { diff --git a/apps/extension/src/provider/Signer.ts b/apps/extension/src/provider/Signer.ts index 0f877f672d..99239519ae 100644 --- a/apps/extension/src/provider/Signer.ts +++ b/apps/extension/src/provider/Signer.ts @@ -25,14 +25,16 @@ export class Signer implements ISigner { constructor( protected readonly chainId: string, private readonly _anoma: Anoma - ) {} + ) { } public async accounts(): Promise { return (await this._anoma.accounts(this.chainId))?.map( - ({ alias, address, chainId, type }) => ({ + ({ alias, address, chainId, type, publicKey }) => ({ alias, address, chainId, + type, + publicKey, isShielded: type === AccountType.ShieldedKeys, }) ); @@ -65,15 +67,22 @@ export class Signer implements ISigner { /** * Submit a transfer */ - public async submitTransfer(args: TransferProps): Promise { + public async submitTransfer( + args: TransferProps, + type: AccountType + ): Promise { const transferMsgValue = new TransferMsgValue(args); const transferMessage = new Message(); + const serializedTransfer = transferMessage.encode( SubmitTransferMsgSchema, transferMsgValue ); - return await this._anoma.submitTransfer(toBase64(serializedTransfer)); + return await this._anoma.submitTransfer({ + txMsg: toBase64(serializedTransfer), + type, + }); } /** diff --git a/apps/extension/src/provider/data.mock.ts b/apps/extension/src/provider/data.mock.ts index f46ebdcabf..4175e24103 100644 --- a/apps/extension/src/provider/data.mock.ts +++ b/apps/extension/src/provider/data.mock.ts @@ -1,7 +1,10 @@ import { AccountType, BridgeType, Chain, Extensions } from "@anoma/types"; import { KdfType, KeyStore } from "background/keyring"; -export const ACTIVE_ACCOUNT_ID = "324bfe0e-cb19-5f1a-9630-9daaaecadabe"; +export const ACTIVE_ACCOUNT = { + id: "324bfe0e-cb19-5f1a-9630-9daaaecadabe", + type: AccountType.Mnemonic, +}; export const chain: Chain = { alias: "Namada Testnet", @@ -30,7 +33,8 @@ export const keyStore: KeyStore[] = [ owner: "atest1d9khqw36gvurxv6yxcmyvv6pgdzyxwp3g9z5gv6px5cyxs348yenjd6ygse5y32xxapy2dph26clnv", chainId: "namada-75a7e12.69483d59a9fb174", - id: ACTIVE_ACCOUNT_ID, + id: ACTIVE_ACCOUNT.id, + parentId: undefined, path: { account: 0, change: 0, @@ -60,7 +64,8 @@ export const keyStore: KeyStore[] = [ }, }, }, - type: AccountType.Mnemonic, + publicKey: undefined, + type: ACTIVE_ACCOUNT.type, }, { alias: "Derived Account", @@ -70,7 +75,7 @@ export const keyStore: KeyStore[] = [ "atest1d9khqw36geprgd2yxgcyy3fnxymnzwf4xpprgdzxgccryvp489qn2wzyxcmrgd2xxdznvv2p8akmfs", chainId: "namada-75a7e12.69483d59a9fb174", id: "123e4567-e89b-12d3-a456-426614174000", - parentId: ACTIVE_ACCOUNT_ID, + parentId: ACTIVE_ACCOUNT.id, path: { account: 0, change: 0, @@ -101,6 +106,7 @@ export const keyStore: KeyStore[] = [ }, }, }, + publicKey: undefined, type: AccountType.PrivateKey, }, ]; diff --git a/apps/extension/src/provider/messages.ts b/apps/extension/src/provider/messages.ts index cb2df89272..e15b6c0291 100644 --- a/apps/extension/src/provider/messages.ts +++ b/apps/extension/src/provider/messages.ts @@ -1,4 +1,4 @@ -import { Chain, DerivedAccount } from "@anoma/types"; +import { AccountType, Chain, DerivedAccount } from "@anoma/types"; import { Message } from "router"; /** @@ -19,6 +19,7 @@ enum MessageType { ApproveTransfer = "approve-tx", QueryBalances = "query-balances", SubmitIbcTransfer = "submit-ibc-transfer", + SubmitLedgerTransfer = "submit-ledger-transfer", EncodeInitAccount = "encode-init-account", EncodeRevealPublicKey = "encode-reveal-public-key", GetChain = "get-chain", @@ -313,7 +314,10 @@ export class ApproveTransferMsg extends Message { return MessageType.ApproveTransfer; } - constructor(public readonly txMsg: string) { + constructor( + public readonly txMsg: string, + public readonly accountType?: AccountType + ) { super(); } diff --git a/apps/extension/src/test/init.ts b/apps/extension/src/test/init.ts index d70b7ad798..4ae0e13a4a 100644 --- a/apps/extension/src/test/init.ts +++ b/apps/extension/src/test/init.ts @@ -1,29 +1,27 @@ +import { chains } from "@anoma/chains"; +import { Query, Sdk } from "@anoma/shared"; import { KVStore } from "@anoma/storage"; +import { Chain } from "@anoma/types"; import { ExtensionRouter, ExtensionMessengerMock, ExtensionRequester, getAnomaRouterId, -} from "../extension"; -import { Ports, KVPrefix } from "../router"; -import { chains } from "@anoma/chains"; -import { ChainsService, init as initChains } from "../background/chains"; +} from "extension"; +import { Ports, KVPrefix } from "router"; +import { ChainsService, init as initChains } from "background/chains"; import { KeyRingService, init as initKeyRing, KeyStore, TabStore, -} from "../background/keyring"; - -import { - ApprovalsService, - init as initApprovals, -} from "../background/approvals"; - + ActiveAccountStore, + AccountStore, +} from "background/keyring"; +import { ApprovalsService, init as initApprovals } from "background/approvals"; import { Anoma } from "provider"; -import { Chain } from "@anoma/types"; -import { Query, Sdk } from "@anoma/shared"; +import { LedgerService } from "background/ledger"; // __wasm is not exported in crypto.d.ts so need to use require instead of import /* eslint-disable @typescript-eslint/no-var-requires */ @@ -51,7 +49,7 @@ export const init = async (): Promise<{ anoma: Anoma; iDBStore: KVStoreMock; extStore: KVStoreMock; - activeAccountStore: KVStoreMock; + activeAccountStore: KVStoreMock; chainsService: ChainsService; keyRingService: KeyRingService; }> => { @@ -59,7 +57,9 @@ export const init = async (): Promise<{ const iDBStore = new KVStoreMock(KVPrefix.IndexedDB); const sdkStore = new KVStoreMock>(KVPrefix.SDK); const extStore = new KVStoreMock(KVPrefix.IndexedDB); - const activeAccountStore = new KVStoreMock(KVPrefix.ActiveAccount); + const activeAccountStore = new KVStoreMock( + KVPrefix.ActiveAccount + ); const connectedTabsStore = new KVStoreMock( KVPrefix.ConnectedTabs ); @@ -98,12 +98,21 @@ export const init = async (): Promise<{ cryptoMemory, requester ); + + const ledgerService = new LedgerService( + keyRingService, + iDBStore as KVStore, + connectedTabsStore, + txStore, + chainId, + sdk + ); + const approvalsService = new ApprovalsService( txStore, connectedTabsStore, keyRingService, - chainId, - requester + ledgerService ); // Initialize messages and handlers diff --git a/apps/extension/src/utils/index.ts b/apps/extension/src/utils/index.ts index 4f9c874c4a..735c32f7bb 100644 --- a/apps/extension/src/utils/index.ts +++ b/apps/extension/src/utils/index.ts @@ -1,12 +1,50 @@ import browser from "webextension-polyfill"; +import { v5 as uuid } from "uuid"; + +import { DerivedAccount } from "@anoma/types"; +import { AccountStore } from "background/keyring"; +import { pick } from "@anoma/utils"; /** - * Extension-specific utilities + * Query the current extension tab and close it */ - export const closeCurrentTab = async (): Promise => { const tab = await browser.tabs.getCurrent(); if (tab.id) { browser.tabs.remove(tab.id); } }; + +/** + * Return all unencrypted values from key store + */ +export const getAccountValuesFromStore = ( + accounts: AccountStore[] +): DerivedAccount[] => { + return accounts.map((account) => + pick( + account, + "address", + "alias", + "chainId", + "id", + "owner", + "parentId", + "publicKey", + "path", + "type" + ) + ); +}; + +/** + * Construct unique uuid (v5), passing in an arbitray number of arguments. + * This could be a unique parameter of the object receiving the id, + * or an index based on the number of existing objects in a hierarchy. + */ +export const generateId = ( + namespace: string, + ...args: (number | string)[] +): string => { + return uuid(args.join(":"), namespace); +}; diff --git a/apps/namada-interface/src/slices/accounts.ts b/apps/namada-interface/src/slices/accounts.ts index bfa58b0606..f2936cfd03 100644 --- a/apps/namada-interface/src/slices/accounts.ts +++ b/apps/namada-interface/src/slices/accounts.ts @@ -80,7 +80,8 @@ const accountsSlice = createSlice({ state.derived[accounts[0].chainId] = {}; accounts.forEach((account) => { - const { address, alias, isShielded, chainId } = account; + const { address, alias, isShielded, chainId, type, publicKey } = + account; const currencySymbol = chains[chainId].currency.symbol; if (!state.derived[chainId]) { state.derived[chainId] = {}; @@ -91,6 +92,8 @@ const accountsSlice = createSlice({ address, alias, chainId, + type, + publicKey, isShielded, }, balance: { diff --git a/apps/namada-interface/src/slices/transfers.ts b/apps/namada-interface/src/slices/transfers.ts index 75851a928c..f5c6e234e1 100644 --- a/apps/namada-interface/src/slices/transfers.ts +++ b/apps/namada-interface/src/slices/transfers.ts @@ -155,19 +155,23 @@ export const submitTransferTransaction = createAsyncThunk< const integration = getIntegration(chainId); const signer = integration.signer() as Signer; - await signer.submitTransfer({ - tx: { + await signer.submitTransfer( + { + tx: { + token: Tokens.NAM.address || "", + feeAmount: new BigNumber(0), + gasLimit: new BigNumber(0), + chainId, + publicKey: txTransferArgs.account.publicKey, + }, + source: txTransferArgs.account.address, + target: txTransferArgs.target, token: Tokens.NAM.address || "", - feeAmount: new BigNumber(0), - gasLimit: new BigNumber(0), - chainId, + amount: txTransferArgs.amount, + nativeToken: Tokens.NAM.address || "", }, - source: txTransferArgs.account.address, - target: txTransferArgs.target, - token: Tokens.NAM.address || "", - amount: txTransferArgs.amount, - nativeToken: Tokens.NAM.address || "", - }); + txTransferArgs.account.type + ); } ); diff --git a/apps/namada-interface/src/store/mocks.ts b/apps/namada-interface/src/store/mocks.ts index 8e2861067b..3d969bd5e9 100644 --- a/apps/namada-interface/src/store/mocks.ts +++ b/apps/namada-interface/src/store/mocks.ts @@ -1,5 +1,6 @@ import BigNumber from "bignumber.js"; +import { AccountType } from "@anoma/types"; import { RootState } from "./store"; import { TransferType } from "slices/transfers"; import { StakingOrUnstakingState } from "slices/StakingAndGovernance"; @@ -9,20 +10,21 @@ export const mockAppState: RootState = { derived: { "anoma-masp-1.5.32ccad5356012a7": { atest1v4ehgw36xqcyz3zrxsenzd3kxsunsvzzxymyywpkg4zrjv2pxepyyd3cgse5gwzxgsm5x3zrkf2pwp: - { - details: { - chainId: "anoma-masp-1.5.32ccad5356012a7", - alias: "Namada", - address: - "atest1v4ehgw36xqcyz3zrxsenzd3kxsunsvzzxymyywpkg4zrjv2pxepyyd3cgse5gwzxgsm5x3zrkf2pwp", - isShielded: false, - }, - balance: { - NAM: new BigNumber(1000), - ATOM: new BigNumber(1000), - ETH: new BigNumber(1000), - }, + { + details: { + chainId: "anoma-masp-1.5.32ccad5356012a7", + alias: "Namada", + address: + "atest1v4ehgw36xqcyz3zrxsenzd3kxsunsvzzxymyywpkg4zrjv2pxepyyd3cgse5gwzxgsm5x3zrkf2pwp", + type: AccountType.PrivateKey, + isShielded: false, }, + balance: { + NAM: new BigNumber(1000), + ATOM: new BigNumber(1000), + ETH: new BigNumber(1000), + }, + }, }, "anoma-test.1e670ba91369ec891fc": { "39UL18": { @@ -31,6 +33,7 @@ export const mockAppState: RootState = { alias: "Namada", address: "atest1v4ehgw36xqcyz3zrxsenzd3kxsunsvzzxymyywpkg4zrjv2pxepyyd3cgse5gwzxgsm5x3zrkf2pwp", + type: AccountType.PrivateKey, isShielded: false, }, balance: { @@ -46,6 +49,7 @@ export const mockAppState: RootState = { chainId: "anoma-test.89060614ce340f4baae", alias: "Namada", address: "L1qDtV8TRwYLSHdMDW518hgRw9nWnRjFTenkcBYNJruyYoLjaj8F", + type: AccountType.PrivateKey, isShielded: false, }, diff --git a/packages/integrations/src/Keplr.test.ts b/packages/integrations/src/Keplr.test.ts index dd3f924a06..078b99dad5 100644 --- a/packages/integrations/src/Keplr.test.ts +++ b/packages/integrations/src/Keplr.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { mock } from "jest-mock-extended"; -import { Chain } from "@anoma/types"; +import { AccountType, Chain } from "@anoma/types"; import { Key, Keplr as IKeplr } from "@keplr-wallet/types"; import Keplr from "./Keplr"; @@ -131,6 +131,7 @@ describe("Keplr class", () => { alias: `${a}...${a}`, chainId: mockChain.chainId, address: a, + type: AccountType.PrivateKey, isShielded: false, })) ); diff --git a/packages/integrations/src/Keplr.ts b/packages/integrations/src/Keplr.ts index 667d39fab5..d9f5eefce3 100644 --- a/packages/integrations/src/Keplr.ts +++ b/packages/integrations/src/Keplr.ts @@ -12,7 +12,13 @@ import { import { Coin } from "@cosmjs/launchpad"; import Long from "long"; -import { Account, Chain, CosmosTokens, TokenBalance } from "@anoma/types"; +import { + Account, + AccountType, + Chain, + CosmosTokens, + TokenBalance, +} from "@anoma/types"; import { shortenAddress } from "@anoma/utils"; import { BridgeProps, Integration } from "./types/Integration"; @@ -35,7 +41,7 @@ class Keplr implements Integration { * override keplr instance for testing * @param chain */ - constructor(public readonly chain: Chain) {} + constructor(public readonly chain: Chain) { } private init(): void { if (!this._keplr) { @@ -116,6 +122,7 @@ class Keplr implements Integration { alias: shortenAddress(account.address, 16), chainId: this.chain.chainId, address: account.address, + type: AccountType.PrivateKey, isShielded: false, }) ); diff --git a/packages/integrations/src/Metamask.ts b/packages/integrations/src/Metamask.ts index ef75ec36e2..c94608eff0 100644 --- a/packages/integrations/src/Metamask.ts +++ b/packages/integrations/src/Metamask.ts @@ -1,7 +1,7 @@ import { type MetaMaskInpageProvider } from "@metamask/providers"; import MetaMaskSDK from "@metamask/sdk"; -import { Account, Chain, TokenBalance } from "@anoma/types"; +import { Account, AccountType, Chain, TokenBalance } from "@anoma/types"; import { shortenAddress } from "@anoma/utils"; import { BridgeProps, Integration } from "./types/Integration"; @@ -15,7 +15,7 @@ type MetamaskWindow = Window & class Metamask implements Integration { private _ethereum: MetaMaskInpageProvider | undefined; - constructor(public readonly chain: Chain) {} + constructor(public readonly chain: Chain) { } private init(): void { if ((window).ethereum) { @@ -48,6 +48,7 @@ class Metamask implements Integration { address, alias: shortenAddress(address, 16), chainId: this.chain.chainId, + type: AccountType.PrivateKey, isShielded: false, })); diff --git a/packages/shared/lib/Cargo.lock b/packages/shared/lib/Cargo.lock index 679360e0c2..4d8855f63f 100644 --- a/packages/shared/lib/Cargo.lock +++ b/packages/shared/lib/Cargo.lock @@ -178,6 +178,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.2" @@ -332,18 +338,34 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" +[[package]] +name = "bellman" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43473b34abc4b0b405efa0a250bac87eea888182b21687ee5c8115d279b0fda5" +dependencies = [ + "bitvec 0.22.3", + "blake2s_simd 0.5.11", + "byteorder", + "ff 0.11.1", + "group 0.11.0", + "pairing 0.21.0", + "rand_core 0.6.4", + "subtle 2.4.1", +] + [[package]] name = "bellman" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4dd656ef4fdf7debb6d87d4dd92642fcbcdb78cbf6600c13e25c87e4d1a3807" dependencies = [ - "bitvec", - "blake2s_simd", + "bitvec 1.0.1", + "blake2s_simd 1.0.1", "byteorder", - "ff", - "group", - "pairing", + "ff 0.12.1", + "group 0.12.1", + "pairing 0.22.0", "rand_core 0.6.4", "subtle 2.4.1", ] @@ -386,16 +408,28 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5237f00a8c86130a0cc317830e558b966dd7850d48a953d998c813f01a41b527" +dependencies = [ + "funty 1.2.0", + "radium 0.6.2", + "tap", + "wyz 0.4.0", +] + [[package]] name = "bitvec" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ - "funty", - "radium", + "funty 2.0.0", + "radium 0.7.0", "tap", - "wyz", + "wyz 0.5.1", ] [[package]] @@ -414,8 +448,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" dependencies = [ "arrayref", - "arrayvec", - "constant_time_eq", + "arrayvec 0.7.2", + "constant_time_eq 0.2.5", +] + +[[package]] +name = "blake2s_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e461a7034e85b211a4acb57ee2e6730b32912b06c08cc242243c39fc21ae6a2" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "constant_time_eq 0.1.5", ] [[package]] @@ -425,8 +470,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637f448b9e61dfadbdcbae9a885fadee1f3eaffb1f8d3c1965d3ade8bdfd44f" dependencies = [ "arrayref", - "arrayvec", - "constant_time_eq", + "arrayvec 0.7.2", + "constant_time_eq 0.2.5", ] [[package]] @@ -499,15 +544,28 @@ dependencies = [ "log", ] +[[package]] +name = "bls12_381" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829c821999c06be34de314eaeb7dd1b42be38661178bc26ad47a4eacebdb0f9" +dependencies = [ + "ff 0.11.1", + "group 0.11.0", + "pairing 0.21.0", + "rand_core 0.6.4", + "subtle 2.4.1", +] + [[package]] name = "bls12_381" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3c196a77437e7cc2fb515ce413a6401291578b5afc8ecb29a3c7ab957f05941" dependencies = [ - "ff", - "group", - "pairing", + "ff 0.12.1", + "group 0.12.1", + "pairing 0.22.0", "rand_core 0.6.4", "subtle 2.4.1", ] @@ -556,8 +614,7 @@ dependencies = [ [[package]] name = "bumpalo" version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +source = "git+https://github.com/fitzgen/bumpalo#fd3dd5767658ad634581d0bc8267c3ed00ec9ec5" [[package]] name = "byte-tools" @@ -692,6 +749,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "constant_time_eq" version = "0.2.5" @@ -1102,13 +1165,24 @@ dependencies = [ "serde_bytes", ] +[[package]] +name = "ff" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" +dependencies = [ + "bitvec 0.22.3", + "rand_core 0.6.4", + "subtle 2.4.1", +] + [[package]] name = "ff" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ - "bitvec", + "bitvec 1.0.1", "rand_core 0.6.4", "subtle 2.4.1", ] @@ -1161,6 +1235,11 @@ dependencies = [ "num-traits", ] +[[package]] +name = "funty" +version = "1.2.0" +source = "git+https://github.com/ferrilab/funty.git?rev=7ef0d890fbcd8b3def1635ac1a877fc298488446#7ef0d890fbcd8b3def1635ac1a877fc298488446" + [[package]] name = "funty" version = "2.0.0" @@ -1313,13 +1392,25 @@ dependencies = [ "web-sys", ] +[[package]] +name = "group" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" +dependencies = [ + "byteorder", + "ff 0.11.1", + "rand_core 0.6.4", + "subtle 2.4.1", +] + [[package]] name = "group" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", "memuse", "rand_core 0.6.4", "subtle 2.4.1", @@ -1475,7 +1566,7 @@ dependencies = [ [[package]] name = "ibc" version = "0.36.0" -source = "git+https://github.com/heliaxdev/cosmos-ibc-rs.git?rev=e71bc2cc79f8c2b32e970d95312f251398c93d9e#e71bc2cc79f8c2b32e970d95312f251398c93d9e" +source = "git+https://github.com/heliaxdev/cosmos-ibc-rs.git?rev=2d7edc16412b60cabf78163fe24a6264e11f77a9#2d7edc16412b60cabf78163fe24a6264e11f77a9" dependencies = [ "bytes", "derive_more", @@ -1504,7 +1595,7 @@ dependencies = [ [[package]] name = "ibc-proto" version = "0.26.0" -source = "git+https://github.com/heliaxdev/ibc-proto-rs.git?rev=6f4038fcf4981f1ed70771d1cd89931267f917af#6f4038fcf4981f1ed70771d1cd89931267f917af" +source = "git+https://github.com/heliaxdev/ibc-proto-rs.git?rev=7e527b5b8c95d83351e93ceafc14ac853224283f#7e527b5b8c95d83351e93ceafc14ac853224283f" dependencies = [ "base64", "bytes", @@ -1665,10 +1756,10 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a575df5f985fe1cd5b2b05664ff6accfc46559032b954529fd225a2168d27b0f" dependencies = [ - "bitvec", - "bls12_381", - "ff", - "group", + "bitvec 1.0.1", + "bls12_381 0.7.1", + "ff 0.12.1", + "group 0.12.1", "rand_core 0.6.4", "subtle 2.4.1", ] @@ -1805,15 +1896,15 @@ source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a dependencies = [ "aes", "bip0039", - "bitvec", + "bitvec 1.0.1", "blake2b_simd", - "blake2s_simd", - "bls12_381", + "blake2s_simd 1.0.1", + "bls12_381 0.7.1", "borsh", "byteorder", - "ff", + "ff 0.12.1", "fpe", - "group", + "group 0.12.1", "hex", "incrementalmerkletree", "jubjub", @@ -1833,17 +1924,16 @@ name = "masp_proofs" version = "0.9.0" source = "git+https://github.com/anoma/masp?rev=9320c6b69b5d2e97134866871e960f0a31703813#9320c6b69b5d2e97134866871e960f0a31703813" dependencies = [ - "bellman", + "bellman 0.13.1", "blake2b_simd", - "bls12_381", + "bls12_381 0.7.1", "directories", "getrandom 0.2.9", - "group", + "group 0.12.1", "itertools", "jubjub", "lazy_static", "masp_primitives", - "minreq", "rand_core 0.6.4", "redjubjub", "tracing", @@ -1898,19 +1988,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "minreq" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de406eeb24aba36ed3829532fa01649129677186b44a49debec0ec574ca7da7" -dependencies = [ - "log", - "once_cell", - "rustls", - "webpki", - "webpki-roots", -] - [[package]] name = "miracl_core" version = "2.3.0" @@ -1930,13 +2007,17 @@ source = "git+https://github.com/anoma/namada#c2779df63b226036c345f4ad645664933c dependencies = [ "async-std", "async-trait", + "bellman 0.11.2", "bimap", + "bls12_381 0.6.1", "borsh", "circular-queue", "clru", "data-encoding", "derivation-path", "derivative", + "ibc", + "ibc-proto", "itertools", "masp_primitives", "masp_proofs", @@ -1952,6 +2033,8 @@ dependencies = [ "serde_json", "sha2 0.9.9", "slip10_ed25519", + "tendermint", + "tendermint-proto", "tendermint-rpc", "thiserror", "tiny-bip39 0.8.2 (git+https://github.com/anoma/tiny-bip39.git?rev=bf0f6d8713589b83af7a917366ec31f5275c0e57)", @@ -1972,6 +2055,7 @@ dependencies = [ "ark-ec", "ark-serialize", "bech32", + "bellman 0.11.2", "borsh", "chrono", "data-encoding", @@ -2026,6 +2110,7 @@ dependencies = [ "borsh", "data-encoding", "derivative", + "hex", "namada_core", "once_cell", "thiserror", @@ -2165,13 +2250,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "pairing" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42" +dependencies = [ + "group 0.11.0", +] + [[package]] name = "pairing" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135590d8bdba2b31346f9cd1fb2a912329f5135e832a4f422942eb6ead8b6b3b" dependencies = [ - "group", + "group 0.12.1", ] [[package]] @@ -2469,6 +2563,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + [[package]] name = "radium" version = "0.7.0" @@ -2630,21 +2730,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", -] - [[package]] name = "ripemd" version = "0.1.3" @@ -2683,18 +2768,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "rustls" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" -dependencies = [ - "log", - "ring", - "sct", - "webpki", -] - [[package]] name = "ryu" version = "1.0.13" @@ -2769,16 +2842,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "semver" version = "0.11.0" @@ -2973,12 +3036,6 @@ dependencies = [ "sha2 0.9.9", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "static_assertions" version = "1.1.0" @@ -3069,7 +3126,7 @@ dependencies = [ [[package]] name = "tendermint" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=02b256829e80f8cfecf3fa0d625c2a76c79cd043#02b256829e80f8cfecf3fa0d625c2a76c79cd043" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=4db3c5ea09fae4057008d22bf9e96bf541b55b35#4db3c5ea09fae4057008d22bf9e96bf541b55b35" dependencies = [ "async-trait", "bytes", @@ -3097,7 +3154,7 @@ dependencies = [ [[package]] name = "tendermint-config" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=02b256829e80f8cfecf3fa0d625c2a76c79cd043#02b256829e80f8cfecf3fa0d625c2a76c79cd043" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=4db3c5ea09fae4057008d22bf9e96bf541b55b35#4db3c5ea09fae4057008d22bf9e96bf541b55b35" dependencies = [ "flex-error", "serde", @@ -3110,7 +3167,7 @@ dependencies = [ [[package]] name = "tendermint-light-client-verifier" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=02b256829e80f8cfecf3fa0d625c2a76c79cd043#02b256829e80f8cfecf3fa0d625c2a76c79cd043" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=4db3c5ea09fae4057008d22bf9e96bf541b55b35#4db3c5ea09fae4057008d22bf9e96bf541b55b35" dependencies = [ "derive_more", "flex-error", @@ -3122,7 +3179,7 @@ dependencies = [ [[package]] name = "tendermint-proto" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=02b256829e80f8cfecf3fa0d625c2a76c79cd043#02b256829e80f8cfecf3fa0d625c2a76c79cd043" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=4db3c5ea09fae4057008d22bf9e96bf541b55b35#4db3c5ea09fae4057008d22bf9e96bf541b55b35" dependencies = [ "bytes", "flex-error", @@ -3139,7 +3196,7 @@ dependencies = [ [[package]] name = "tendermint-rpc" version = "0.23.6" -source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=02b256829e80f8cfecf3fa0d625c2a76c79cd043#02b256829e80f8cfecf3fa0d625c2a76c79cd043" +source = "git+https://github.com/heliaxdev/tendermint-rs.git?rev=4db3c5ea09fae4057008d22bf9e96bf541b55b35#4db3c5ea09fae4057008d22bf9e96bf541b55b35" dependencies = [ "async-trait", "bytes", @@ -3424,12 +3481,6 @@ dependencies = [ "subtle 2.4.1", ] -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "url" version = "2.3.1" @@ -3607,25 +3658,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - [[package]] name = "which" version = "4.4.0" @@ -3809,6 +3841,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "wyz" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129e027ad65ce1453680623c3fb5163cbf7107bfe1aa32257e7d0e63f9ced188" +dependencies = [ + "tap", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/packages/shared/lib/Cargo.toml b/packages/shared/lib/Cargo.toml index 2c1ee464fe..12d98d5d3f 100644 --- a/packages/shared/lib/Cargo.toml +++ b/packages/shared/lib/Cargo.toml @@ -50,12 +50,6 @@ features = [ [dev-dependencies] wasm-bindgen-test = "0.3.13" -[patch.crates-io] -borsh = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} -borsh-derive = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} -borsh-derive-internal = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} -borsh-schema-derive-internal = {git = "https://github.com/heliaxdev/borsh-rs.git", rev = "cd5223e5103c4f139e0c54cf8259b7ec5ec4073a"} - [package.metadata.wasm-pack.profile.release] wasm-opt = true diff --git a/packages/shared/lib/src/sdk/mod.rs b/packages/shared/lib/src/sdk/mod.rs index 460cc6fd69..c39df5bb97 100644 --- a/packages/shared/lib/src/sdk/mod.rs +++ b/packages/shared/lib/src/sdk/mod.rs @@ -1,14 +1,22 @@ -use namada::ledger::{ - masp::ShieldedContext, - wallet::{Store, Wallet}, -}; -use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue}; - +use crate::utils::to_js_result; use crate::{ rpc_client::HttpClient, sdk::masp::WebShieldedUtils, utils::{set_panic_hook, to_bytes}, }; +use borsh::BorshSerialize; +use namada::{ + ledger::{ + args, + masp::ShieldedContext, + signing, + wallet::{Store, Wallet}, + }, + proto::{Section, Signature, Tx}, + types::{key::common::PublicKey, key::common::SecretKey as SK, key::ed25519::SecretKey}, +}; +use std::str::FromStr; +use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue}; pub mod masp; mod tx; @@ -87,28 +95,130 @@ impl Sdk { wallet::add_spending_key(&mut self.wallet, xsk, password, alias) } - pub async fn submit_bond( + async fn submit_reveal_pk( &mut self, - tx_msg: &[u8], - password: Option, + args: &args::Tx, + mut tx: Tx, + pk: &PublicKey, ) -> Result<(), JsError> { - let args = tx::bond_tx_args(tx_msg, password)?; + // Build a transaction to reveal the signer of this transaction + let reveal_pk = namada::ledger::tx::build_reveal_pk( + &self.client, + &mut self.wallet, + args::RevealPk { + tx: args.clone(), + public_key: pk.clone(), + }, + ) + .await?; + + // Sign and submit reveal pk + if let Some((mut rtx, _, pk)) = reveal_pk { + // Sign the reveal public key transaction with the fee payer + signing::sign_tx(&mut self.wallet, &mut rtx, &args, &pk).await?; + // Submit the reveal public key transaction first + namada::ledger::tx::process_tx(&self.client, &mut self.wallet, &args, rtx).await?; + // Update the stateful PoW challenge of the outer transaction + #[cfg(not(feature = "mainnet"))] + signing::update_pow_challenge(&self.client, &args, &mut tx, &pk, false).await; + } + + Ok(()) + } + + /// Sign and submit transactions + async fn sign_and_process_tx( + &mut self, + args: args::Tx, + mut tx: Tx, + pk: PublicKey, + ) -> Result<(), JsError> { + // Submit a reveal pk tx if necessary + self.submit_reveal_pk(&args, tx.clone(), &pk).await?; + + // Sign tx + signing::sign_tx(&mut self.wallet, &mut tx, &args, &pk) + .await + .map_err(JsError::from)?; - namada::ledger::tx::submit_bond(&mut self.client, &mut self.wallet, args) + // Submit tx + namada::ledger::tx::process_tx(&self.client, &mut self.wallet, &args, tx) .await - .map_err(|e| JsError::from(e)) + .map_err(JsError::from)?; + + Ok(()) } - pub async fn submit_unbond( + /// 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) + } + + // Append signatures and return tx bytes + pub fn sign_tx( + &self, + tx_bytes: &[u8], + data_key: String, + code_key: String, + header_key: String, + ) -> Result { + let mut tx: Tx = Tx::try_from(tx_bytes).map_err(JsError::from)?; + let data_secret = SecretKey::from_str(&data_key).map_err(JsError::from)?; + let code_secret = SecretKey::from_str(&code_key).map_err(JsError::from)?; + let header_secret = SecretKey::from_str(&header_key).map_err(JsError::from)?; + + // Sign over the transaction data + tx.add_section(Section::Signature(Signature::new( + tx.data_sechash(), + &SK::Ed25519(data_secret), + ))); + // Sign over the transaction code + tx.add_section(Section::Signature(Signature::new( + tx.code_sechash(), + &SK::Ed25519(code_secret), + ))); + // Then sign over the bound wrapper + tx.add_section(Section::Signature(Signature::new( + &tx.header_hash(), + &SK::Ed25519(header_secret), + ))); + + let bytes = tx.try_to_vec().map_err(|e| JsError::from(e))?; + to_js_result(Vec::from(bytes)) + } + + /// Submit signed transfer tx + pub async fn submit_signed_transfer( &mut self, + pk: String, tx_msg: &[u8], - password: Option, + tx_bytes: &[u8], ) -> Result<(), JsError> { - let args = tx::unbond_tx_args(tx_msg, password)?; + let transfer_tx = Tx::try_from(tx_bytes).map_err(|e| JsError::from(e))?; + let args = tx::transfer_tx_args(tx_msg, None, None).map_err(|e| JsError::from(e))?; + let pk = PublicKey::from_str(&pk).map_err(JsError::from)?; - namada::ledger::tx::submit_unbond(&mut self.client, &mut self.wallet, args) + self.submit_reveal_pk(&args.tx, transfer_tx.clone(), &pk) + .await?; + + namada::ledger::tx::process_tx(&self.client, &mut self.wallet, &args.tx, transfer_tx) .await - .map_err(|e| JsError::from(e)) + .map_err(JsError::from)?; + + Ok(()) } pub async fn submit_transfer( @@ -118,14 +228,18 @@ impl Sdk { xsk: Option, ) -> Result<(), JsError> { let args = tx::transfer_tx_args(tx_msg, password, xsk)?; - namada::ledger::tx::submit_transfer( + let (tx, _, pk, _, _) = namada::ledger::tx::build_transfer( &self.client, &mut self.wallet, &mut self.shielded_ctx, - args, + args.clone(), ) .await - .map_err(|e| JsError::from(e)) + .map_err(JsError::from)?; + + self.sign_and_process_tx(args.tx, tx, pk).await?; + + Ok(()) } pub async fn submit_ibc_transfer( @@ -135,9 +249,48 @@ impl Sdk { ) -> Result<(), JsError> { let args = tx::ibc_transfer_tx_args(tx_msg, password)?; - namada::ledger::tx::submit_ibc_transfer(&self.client, &mut self.wallet, args) - .await - .map_err(|e| JsError::from(e)) + let (tx, _, pk) = + namada::ledger::tx::build_ibc_transfer(&self.client, &mut self.wallet, args.clone()) + .await + .map_err(JsError::from)?; + + self.sign_and_process_tx(args.tx, tx, pk).await?; + + Ok(()) + } + + pub async fn submit_bond( + &mut self, + tx_msg: &[u8], + password: Option, + ) -> Result<(), JsError> { + let args = tx::bond_tx_args(tx_msg, password)?; + + let (tx, _, pk) = + namada::ledger::tx::build_bond(&mut self.client, &mut self.wallet, args.clone()) + .await + .map_err(JsError::from)?; + + self.sign_and_process_tx(args.tx, tx, pk).await?; + + Ok(()) + } + + pub async fn submit_unbond( + &mut self, + tx_msg: &[u8], + password: Option, + ) -> Result<(), JsError> { + let args = tx::unbond_tx_args(tx_msg, password)?; + + let (tx, _, pk, _) = + namada::ledger::tx::build_unbond(&mut self.client, &mut self.wallet, args.clone()) + .await + .map_err(JsError::from)?; + + self.sign_and_process_tx(args.tx, tx, pk).await?; + + Ok(()) } } diff --git a/packages/shared/lib/src/sdk/tx.rs b/packages/shared/lib/src/sdk/tx.rs index d428f373d9..9a44c29db9 100644 --- a/packages/shared/lib/src/sdk/tx.rs +++ b/packages/shared/lib/src/sdk/tx.rs @@ -7,6 +7,8 @@ use namada::{ types::{ address::Address, chain::ChainId, + key::common::PublicKey as PK, + key::ed25519::PublicKey, masp::{ExtendedSpendingKey, PaymentAddress, TransferSource, TransferTarget}, token::{Amount, DenominatedAmount, Denomination}, transaction::GasLimit, @@ -20,6 +22,7 @@ pub struct TxMsg { fee_amount: u64, gas_limit: u64, chain_id: String, + public_key: Option, } #[derive(BorshSerialize, BorshDeserialize)] @@ -175,6 +178,7 @@ pub fn transfer_tx_args( ))), }, }?; + let native_token = Address::from_str(&native_token)?; let token = Address::from_str(&token)?; let amount_str = amount.to_string(); @@ -275,8 +279,8 @@ fn tx_msg_into_args(tx_msg: TxMsg, password: Option) -> Result) -> Result { + let pk = PublicKey::from_str(&v).map_err(JsError::from)?; + Some(PK::Ed25519(pk)) + } + _ => None, + }; let args = args::Tx { dry_run: false, @@ -306,6 +317,7 @@ fn tx_msg_into_args(tx_msg: TxMsg, password: Option) -> Result & { +export type Account = Pick< + DerivedAccount, + "address" | "alias" | "chainId" | "type" | "publicKey" +> & { isShielded: boolean; }; diff --git a/packages/types/src/anoma.ts b/packages/types/src/anoma.ts index e0335a5fb9..cbe134d8dd 100644 --- a/packages/types/src/anoma.ts +++ b/packages/types/src/anoma.ts @@ -1,4 +1,4 @@ -import { DerivedAccount } from "./account"; +import { AccountType, DerivedAccount } from "./account"; import { Chain } from "./chain"; import { Signer } from "./signer"; @@ -13,7 +13,10 @@ export interface Anoma { chains: () => Promise; submitBond: (txMsg: string) => Promise; submitUnbond: (txMsg: string) => Promise; - submitTransfer: (txMsg: string) => Promise; + submitTransfer: (props: { + txMsg: string; + type: AccountType; + }) => Promise; submitIbcTransfer: (txMsg: string) => Promise; encodeInitAccount: (props: { txMsg: string; diff --git a/packages/types/src/signer.ts b/packages/types/src/signer.ts index 163061072f..c0cfa51494 100644 --- a/packages/types/src/signer.ts +++ b/packages/types/src/signer.ts @@ -1,4 +1,4 @@ -import { Account } from "./account"; +import { Account, AccountType } from "./account"; import { IbcTransferProps, InitAccountProps, @@ -11,7 +11,7 @@ export interface Signer { accounts: () => Promise; submitBond(args: SubmitBondProps): Promise; submitUnbond(args: SubmitUnbondProps): Promise; - submitTransfer(args: TransferProps): Promise; + submitTransfer(args: TransferProps, type: AccountType): Promise; submitIbcTransfer(args: IbcTransferProps): Promise; encodeInitAccount( args: InitAccountProps, diff --git a/packages/types/src/tx/schema/index.ts b/packages/types/src/tx/schema/index.ts index 3b18bd5e8e..ed193b270c 100644 --- a/packages/types/src/tx/schema/index.ts +++ b/packages/types/src/tx/schema/index.ts @@ -1,6 +1,5 @@ export * from "./account"; export * from "./ibcTransfer"; -export * from "./shielded"; export * from "./transfer"; export * from "./bond"; export * from "./unbond"; diff --git a/packages/types/src/tx/schema/shielded.ts b/packages/types/src/tx/schema/shielded.ts deleted file mode 100644 index 4c4e0c24d1..0000000000 --- a/packages/types/src/tx/schema/shielded.ts +++ /dev/null @@ -1,86 +0,0 @@ -import BN from "bn.js"; -import { ShieldedDataProps, ShieldedProps } from "../types"; - -// A Zcash Transaction -export class ShieldedTransferMsgValue { - txid: Uint8Array; - data: Uint8Array; - - constructor(properties: ShieldedProps) { - this.txid = properties.txId; - this.data = properties.data; - } -} - -export const ShieldedTransferMsgSchema = new Map([ - [ - ShieldedTransferMsgValue, - { - kind: "struct", - fields: [ - ["txid", ["u8", 32]], - ["data", ["u8"]], - ], - }, - ], -]); - -// Zcash Transaction Data -export class ShieldedDataMsgValue { - overwintered: boolean; - version: string; - version_group_id: string; - vin: Uint8Array; - vout: Uint8Array; - lock_time: BN; - expiry_height: BN; - value_balance: BN; - shielded_spends: Uint8Array; - shielded_converts: Uint8Array; - shielded_outputs: Uint8Array; - join_splits: string; - join_split_pubkey?: Uint8Array; - join_split_sig?: Uint8Array; - binding_sig?: Uint8Array; - - constructor(properties: ShieldedDataProps) { - this.overwintered = properties.overwintered; - this.version = properties.version; - this.version_group_id = properties.versionGroupId; - this.vin = properties.vin; - this.vout = properties.vout; - this.lock_time = new BN(properties.lockTime); - this.expiry_height = new BN(properties.expiryHeight); - this.value_balance = new BN(properties.valueBalance.toString()); - this.shielded_spends = properties.shieldedSpends; - this.shielded_converts = properties.shieldedConverts; - this.shielded_outputs = properties.shieldedOutputs; - this.join_splits = properties.joinSplits; - this.join_split_pubkey = properties.joinSplitPubKey; - this.join_split_sig = properties.joinSplitSig; - this.binding_sig = properties.bindingSig; - } -} - -export const ShieldedDataMsg = new Map([ - [ - ShieldedDataMsgValue, - [ - ["overwintered", "boolean"], - ["version", "string"], - ["version_group_id", ["u8"]], - ["vin", ["u8"]], - ["vout", ["u8"]], - ["lock_time", "u64"], - ["expiry_height", "u64"], - ["value_balance", "u64"], - ["shielded_spends", ["u8"]], - ["shielded_converts", ["u8"]], - ["shielded_outputs", ["u8"]], - ["join_splits", "string"], - ["join_split_pubkey", { kind: "option", type: ["u8", 32] }], - ["join_split_sig", { kind: "option", type: ["u8", 64] }], - ["binding_sig", { kind: "option", type: ["u8"] }], - ], - ], -]); diff --git a/packages/types/src/tx/schema/tx.ts b/packages/types/src/tx/schema/tx.ts index 24d3c47f6f..b3e055e98e 100644 --- a/packages/types/src/tx/schema/tx.ts +++ b/packages/types/src/tx/schema/tx.ts @@ -7,18 +7,22 @@ export class TxMsgValue { fee_amount: BN; gas_limit: BN; chain_id: string; + public_key?: string; constructor(properties: TxProps | SchemaObject) { this.token = properties.token; - this.fee_amount = 'feeAmount' in properties ? - new BN(properties.feeAmount.toString()) : - properties.fee_amount; - this.gas_limit = 'gasLimit' in properties ? - new BN(properties.gasLimit.toString()) : - properties.gas_limit; - this.chain_id = 'chainId' in properties ? - properties.chainId : - properties.chain_id; + this.fee_amount = + "feeAmount" in properties + ? new BN(properties.feeAmount.toString()) + : properties.fee_amount; + this.gas_limit = + "gasLimit" in properties + ? new BN(properties.gasLimit.toString()) + : properties.gas_limit; + this.chain_id = + "chainId" in properties ? properties.chainId : properties.chain_id; + this.public_key = + "publicKey" in properties ? properties.publicKey : undefined; } } @@ -31,6 +35,7 @@ export const TxMsgSchema = [ ["fee_amount", "u64"], ["gas_limit", "u64"], ["chain_id", "string"], + ["public_key", { kind: "option", type: "string" }], ], }, ] as const; // needed for SchemaObject to deduce types correctly diff --git a/packages/types/src/tx/types.ts b/packages/types/src/tx/types.ts index 45e32a4bf2..0f8ec07d9d 100644 --- a/packages/types/src/tx/types.ts +++ b/packages/types/src/tx/types.ts @@ -20,6 +20,7 @@ export type TxProps = { feeAmount: BigNumber; gasLimit: BigNumber; chainId: string; + publicKey?: string; }; export type TransferProps = { @@ -57,26 +58,3 @@ export type BridgeTransferProps = { export type InitAccountProps = { vpCode: Uint8Array; }; - -export type ShieldedDataProps = { - overwintered: boolean; - version: string; - versionGroupId: string; - vin: Uint8Array; - vout: Uint8Array; - lockTime: number; - expiryHeight: number; - valueBalance: BigNumber; - shieldedSpends: Uint8Array; - shieldedConverts: Uint8Array; - shieldedOutputs: Uint8Array; - joinSplits: string; - joinSplitPubKey?: Uint8Array; - joinSplitSig?: Uint8Array; - bindingSig?: Uint8Array; -}; - -export type ShieldedProps = { - txId: Uint8Array; - data: Uint8Array; // Encoded ShieldedData -}; diff --git a/packages/utils/src/helpers/index.ts b/packages/utils/src/helpers/index.ts index ab1e5e7a67..e2a614ccb2 100644 --- a/packages/utils/src/helpers/index.ts +++ b/packages/utils/src/helpers/index.ts @@ -1,8 +1,12 @@ import { JsonRpcRequest } from "@cosmjs/json-rpc"; import { DateTime } from "luxon"; -import { JsonCompatibleArray, JsonCompatibleDictionary } from "@anoma/types"; import BigNumber from "bignumber.js"; import BN from "bn.js"; +import { + Bip44Path, + JsonCompatibleArray, + JsonCompatibleDictionary, +} from "@anoma/types"; const MICRO_FACTOR = 1000000; // 1,000,000 @@ -174,14 +178,13 @@ export const Result = { }, }; - // Translates Borsh type to JS type type FromBorsh = T extends "u8" | "u16" | "u32" ? number : T extends "u64" | "u128" | "u256" | "u512" ? BN : T extends "string" ? string : T extends { kind: "option", type: infer S } ? FromBorsh : - T extends new(...args: infer _) => infer Res ? Res : + T extends new (...args: infer _) => infer Res ? Res : T extends readonly unknown[] ? Uint8Array : unknown; @@ -229,14 +232,41 @@ type Value = */ export type SchemaObject = T extends readonly [unknown, { kind: "struct", fields: infer Fields }] ? - Fields extends readonly (infer FieldEntry extends (readonly [string, unknown]))[] ? - { - // optional types need special handling to add the '?' suffix to their keys - [KV in FieldEntry as Value extends { kind: "option" } ? Key : never]?: - Value extends { type: infer S } ? FromBorsh : unknown; - } & { - [KV in FieldEntry as Value extends { kind: "option" } ? never : Key]: - FromBorsh>; - } : - never : - never; + Fields extends readonly (infer FieldEntry extends (readonly [string, unknown]))[] ? + { + // optional types need special handling to add the '?' suffix to their keys + [KV in FieldEntry as Value extends { kind: "option" } ? Key : never]?: + Value extends { type: infer S } ? FromBorsh : unknown; + } & { + [KV in FieldEntry as Value extends { kind: "option" } ? never : Key]: + FromBorsh>; + } : + never : + never; + +/** + * Return a properly formatted BIP-044 path + * + * @param {number} coinType - SLIP-044 Coin designation + * @param {Bip44Path} bip44Path - path object + * @returns {string} + */ +export const makeBip44Path = ( + coinType: number, + bip44Path: Bip44Path +): string => { + const { account, change, index } = bip44Path; + const basePath = `m/44'/${coinType}'/${account}'/${change}`; + + return typeof index === "number" ? `${basePath}/${index}` : basePath; +}; + +/** + * Pick object parameters + */ +export function pick(obj: T, ...keys: K[]): Pick { + return keys.reduce((acc, val) => { + return (acc[val] = obj[val]), acc; + }, {} as Pick); +} +