diff --git a/src/components/AccountCard/ApproveExecuteForm/types.tsx b/src/components/AccountCard/ApproveExecuteForm/types.tsx deleted file mode 100644 index 35beb6e98..000000000 --- a/src/components/AccountCard/ApproveExecuteForm/types.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ApproveOrExecute } from "../../../utils/tezos/types"; -import { MultisigOperation } from "../../../utils/multisig/types"; -import { ContractAddress, ImplicitAddress } from "../../../types/Address"; - -export type ParamsWithFee = ApproveExecuteParams & { suggestedFeeMutez: number }; - -export type ApproveExecuteParams = { - type: ApproveOrExecute; - operation: MultisigOperation; - signer: ImplicitAddress; - multisigAddress: ContractAddress; -}; diff --git a/src/components/AccountCard/ApproveExecuteForm/useApproveOrExecuteModal.tsx b/src/components/AccountCard/ApproveExecuteForm/useApproveOrExecuteModal.tsx deleted file mode 100644 index 2607ca6b1..000000000 --- a/src/components/AccountCard/ApproveExecuteForm/useApproveOrExecuteModal.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useToast } from "@chakra-ui/react"; -import { useState } from "react"; -import ApproveExecuteForm from "./ApproveExecuteForm"; -import { useGetPk } from "../../../utils/hooks/accountHooks"; -import { useSelectedNetwork } from "../../../utils/hooks/assetsHooks"; -import { estimateMultisigApproveOrExecute } from "../../../utils/tezos"; -import { ApproveExecuteParams } from "./types"; -import { useModal } from "./useModal"; - -export const useApproveOrExecuteModdal = () => { - const { modalElement, onOpen } = useModal(ApproveExecuteForm); - const toast = useToast(); - const [isLoading, setIsLoading] = useState(false); - const network = useSelectedNetwork(); - const getPk = useGetPk(); - - const approveOrExecute = async (params: ApproveExecuteParams) => { - setIsLoading(true); - try { - const pk = getPk(params.signer.pkh); - const { suggestedFeeMutez } = await estimateMultisigApproveOrExecute( - { - type: params.type, - contract: params.multisigAddress, - operationId: params.operation.id, - }, - pk, - params.signer.pkh, - network - ); - onOpen({ ...params, suggestedFeeMutez }); - } catch (error: any) { - console.warn("Failed simulation", error); - toast({ title: "Failed simulation", description: error.message, status: "warning" }); - } - - setIsLoading(false); - }; - - return { modalElement, isLoading, approveOrExecute }; -}; diff --git a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigActionButton.test.tsx b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigActionButton.test.tsx index 6d75fd37b..8d8157749 100644 --- a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigActionButton.test.tsx +++ b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigActionButton.test.tsx @@ -1,76 +1,80 @@ -import { mockImplicitAccount } from "../../../../mocks/factories"; +import { mockImplicitAccount, mockMultisigAccount } from "../../../../mocks/factories"; import { render, screen } from "../../../../mocks/testUtils"; import store from "../../../../utils/redux/store"; import MultisigActionButton from "./MultisigSignerTile"; import accountsSlice from "../../../../utils/redux/slices/accountsSlice"; +import { pendingOps } from "../../../../mocks/multisig"; const { add } = accountsSlice.actions; +const account = mockImplicitAccount(0); describe("", () => { it("should display execute for non-pending operation with signer included in the owned account", () => { - const account = mockImplicitAccount(0); store.dispatch(add([account])); render( {}} + openSignModal={_ => {}} + operation={pendingOps[0]} + account={mockMultisigAccount(0)} /> ); expect(screen.getByTestId("multisig-signer-button")).toHaveTextContent("Execute"); }); it("should display approve for pending operation with signer included in the owned account", () => { - const account = mockImplicitAccount(0); store.dispatch(add([account])); render( {}} + openSignModal={_ => {}} + operation={pendingOps[0]} + account={mockMultisigAccount(0)} /> ); expect(screen.getByTestId("multisig-signer-button")).toHaveTextContent("Approve"); }); it("should show approved for pending operation with signers included in the account that already approved", () => { - const account = mockImplicitAccount(0); store.dispatch(add([account])); + const operation = { ...pendingOps[0], approvals: [account.address] }; render( {}} + openSignModal={_ => {}} + operation={operation} + account={mockMultisigAccount(0)} /> ); expect(screen.getByTestId("multisig-signer-approved")).toHaveTextContent("Approved"); }); it("should show approved for operation with signers not in the account", () => { - const account = mockImplicitAccount(0); + const operation = { ...pendingOps[0], approvals: [account.address] }; render( {}} + openSignModal={_ => {}} + operation={operation} + account={mockMultisigAccount(0)} /> ); expect(screen.getByTestId("multisig-signer-approved-or-waiting")).toHaveTextContent("Approved"); }); it("should show Awaiting approval for operation with signers not owned by the user account that hasn't approved", () => { - const account = mockImplicitAccount(0); render( {}} + openSignModal={_ => {}} + operation={pendingOps[0]} + account={mockMultisigAccount(0)} /> ); expect(screen.getByTestId("multisig-signer-approved-or-waiting")).toHaveTextContent( diff --git a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigActionButton.tsx b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigActionButton.tsx index 2c63c5bd5..b95820347 100644 --- a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigActionButton.tsx +++ b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigActionButton.tsx @@ -5,20 +5,32 @@ import { RxCheckCircled } from "react-icons/rx"; import colors from "../../../../style/colors"; import { ImplicitAddress } from "../../../../types/Address"; import { useGetImplicitAccount } from "../../../../utils/hooks/accountHooks"; -import { ApproveOrExecute } from "../../../../utils/tezos/types"; +import { useSelectedNetwork } from "../../../../utils/hooks/assetsHooks"; +import { estimateMultisigApproveOrExecute } from "../../../../utils/tezos"; import { IconAndTextBtn } from "../../../IconAndTextBtn"; +import { ParamsWithFee } from "../../../ApproveExecuteForm/types"; +import { MultisigOperation } from "../../../../utils/multisig/types"; +import { MultisigAccount } from "../../../../types/Account"; export const MultisigActionButton: React.FC<{ - signer: ImplicitAddress; // TODO: change to ImplicitAccount - approvers: ImplicitAddress[]; // TODO: change to ImplicitAccount + signerAddress: ImplicitAddress; pendingApprovals: number; - onClickApproveOrExecute: (a: ApproveOrExecute) => void; - isLoading?: boolean; -}> = ({ signer, approvers, pendingApprovals, onClickApproveOrExecute, isLoading = false }) => { + operation: MultisigOperation; + account: MultisigAccount; + openSignModal: (params: ParamsWithFee) => void; +}> = ({ + signerAddress, + account: { address: multisigAddress }, + operation, + pendingApprovals, + openSignModal, +}) => { const getImplicitAccount = useGetImplicitAccount(); + const network = useSelectedNetwork(); + const signer = getImplicitAccount(signerAddress.pkh); + const signerInOwnedAccounts = !!signer; - const signerInOwnedAccounts = !!getImplicitAccount(signer.pkh); - const approvedBySigner = !!approvers.find(approver => approver === signer); + const approvedBySigner = !!operation.approvals.find(approver => approver === signerAddress); const operationIsExecutable = pendingApprovals === 0; if (!signerInOwnedAccounts) { @@ -47,13 +59,28 @@ export const MultisigActionButton: React.FC<{ ); } + const onButtonClick = async () => { + const actionType = operationIsExecutable ? "execute" : "approve"; + const { suggestedFeeMutez } = await estimateMultisigApproveOrExecute( + { + type: actionType, + contract: multisigAddress, + operationId: operation.id, + }, + signer, + network + ); + openSignModal({ + type: actionType, + operation: operation, + multisigAddress, + signer, + suggestedFeeMutez, + }); + }; + return ( - ); diff --git a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigPendingAccordionItem.test.tsx b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigPendingAccordionItem.test.tsx index 02912b2f1..f9661781d 100644 --- a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigPendingAccordionItem.test.tsx +++ b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigPendingAccordionItem.test.tsx @@ -1,17 +1,18 @@ import { Accordion } from "@chakra-ui/react"; -import { Estimate, TransactionOperation } from "@taquito/taquito"; +import { Estimate, TezosToolkit, TransactionOperation } from "@taquito/taquito"; import { mockContractAddress, mockImplicitAccount, mockImplicitAddress, + mockMultisigAccount, } from "../../../../mocks/factories"; import { fakeTezosUtils } from "../../../../mocks/fakeTezosUtils"; import { fillPassword } from "../../../../mocks/helpers"; import { pendingOps } from "../../../../mocks/multisig"; -import { act, fireEvent, render, screen, waitFor, within } from "../../../../mocks/testUtils"; +import { fireEvent, render, screen, waitFor, within } from "../../../../mocks/testUtils"; import { ImplicitAccount } from "../../../../types/Account"; import { parseImplicitPkh } from "../../../../types/Address"; -import { useGetSk } from "../../../../utils/hooks/accountUtils"; +import { useGetSecretKey } from "../../../../utils/hooks/accountUtils"; import { MultisigOperation } from "../../../../utils/multisig/types"; import accountsSlice from "../../../../utils/redux/slices/accountsSlice"; import store from "../../../../utils/redux/store"; @@ -19,27 +20,26 @@ import MultisigPendingAccordionItem from "./MultisigPendingAccordionItem"; jest.mock("../../../../utils/hooks/accountUtils"); +const MOCK_TEZOS_TOOLKIT = {}; beforeEach(() => { - (useGetSk as jest.Mock).mockReturnValue(() => Promise.resolve("mockkey")); + (useGetSecretKey as jest.Mock).mockReturnValue(() => Promise.resolve("mockkey")); + fakeTezosUtils.makeToolkit.mockResolvedValue(MOCK_TEZOS_TOOLKIT as TezosToolkit); }); describe("", () => { it("displays the correct number of pending approvals", () => { const pkh0 = mockImplicitAddress(0); - const pkh1 = mockImplicitAddress(1); - const pkh2 = mockImplicitAddress(2); + const account = { ...mockMultisigAccount(0), threshold: 3 }; render( ); @@ -48,21 +48,16 @@ describe("", () => { }); it("displays 0 for pending approvals if there are more approvers than the threshold", () => { - const pkh0 = mockImplicitAddress(0); - const pkh1 = mockImplicitAddress(1); - const pkh2 = mockImplicitAddress(2); render( ); @@ -83,19 +78,14 @@ describe("", () => { hash: "mockHash", } as TransactionOperation); - act(() => { - store.dispatch(accountsSlice.actions.add([account])); - }); + store.dispatch(accountsSlice.actions.add([account])); const executablePendingOp: MultisigOperation = pendingOps[0]; + const multisig = { ...mockMultisigAccount(0), signers: [account.address] }; + render( - + ); const firstPendingOp = screen.getByTestId("multisig-pending-operation-" + pendingOps[0].id); @@ -112,8 +102,7 @@ describe("", () => { operationId: executablePendingOp.id, type: "execute", }, - account.pk, - account.address.pkh, + account, "mainnet" ); @@ -134,12 +123,12 @@ describe("", () => { operationId: executablePendingOp.id, type: "execute", }, - { network: "mainnet", sk: "mockkey", type: "sk" } + MOCK_TEZOS_TOOLKIT ); }); test("User can accomplish a proposal approval", async () => { - const account: ImplicitAccount = { + const signer: ImplicitAccount = { ...mockImplicitAccount(0), address: parseImplicitPkh("tz1UNer1ijeE9ndjzSszRduR3CzX49hoBUB3"), }; @@ -151,19 +140,12 @@ describe("", () => { hash: "mockHash", } as TransactionOperation); - act(() => { - store.dispatch(accountsSlice.actions.add([account])); - }); - + store.dispatch(accountsSlice.actions.add([signer])); + const account = { ...mockMultisigAccount(0), signers: [signer.address] }; const approvablePendingOp: MultisigOperation = { ...pendingOps[0], approvals: [] }; render( - + ); const firstPendingOp = screen.getByTestId("multisig-pending-operation-" + pendingOps[0].id); @@ -180,8 +162,7 @@ describe("", () => { operationId: approvablePendingOp.id, type: "approve", }, - account.pk, - account.address.pkh, + signer, "mainnet" ); @@ -202,7 +183,7 @@ describe("", () => { operationId: approvablePendingOp.id, type: "approve", }, - { network: "mainnet", sk: "mockkey", type: "sk" } + MOCK_TEZOS_TOOLKIT ); }); }); diff --git a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigPendingAccordionItem.tsx b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigPendingAccordionItem.tsx index bd915be43..bf3b449a1 100644 --- a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigPendingAccordionItem.tsx +++ b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigPendingAccordionItem.tsx @@ -9,21 +9,22 @@ import { AccordionPanel, } from "@chakra-ui/react"; import React from "react"; -import colors from "../../../../style/colors"; import { MultisigOperation } from "../../../../utils/multisig/types"; import MultisigSignerTile from "./MultisigSignerTile"; -import { ContractAddress, ImplicitAddress } from "../../../../types/Address"; import MultisigDecodedOperations from "./MultisigDecodedOperations"; -import { useApproveOrExecuteModdal } from "../../ApproveExecuteForm/useApproveOrExecuteModal"; +import { MultisigAccount } from "../../../../types/Account"; +import ApproveExecuteForm from "../../../ApproveExecuteForm/ApproveExecuteForm"; +import { useModal } from "../../../useModal"; +import colors from "../../../../style/colors"; export const MultisigPendingAccordionItem: React.FC<{ operation: MultisigOperation; - signers: ImplicitAddress[]; - threshold: number; - multisigAddress: ContractAddress; -}> = ({ operation, signers, threshold, multisigAddress }) => { + account: MultisigAccount; +}> = ({ operation, account }) => { + const { modalElement, onOpen } = useModal(ApproveExecuteForm); + + const { signers, threshold } = account; const pendingApprovals = Math.max(threshold - operation.approvals.length, 0); - const { isLoading, modalElement, approveOrExecute } = useApproveOrExecuteModdal(); return ( - +

@@ -58,14 +59,12 @@ export const MultisigPendingAccordionItem: React.FC<{ {signers.map(signer => ( { - approveOrExecute({ type: a, operation, signer, multisigAddress }); - }} + openSignModal={onOpen} + account={account} + operation={operation} /> ))} diff --git a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigSignerTile.test.tsx b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigSignerTile.test.tsx index 64bcdf3f8..898f86ba3 100644 --- a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigSignerTile.test.tsx +++ b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigSignerTile.test.tsx @@ -1,49 +1,54 @@ -import { mockImplicitAccount } from "../../../../mocks/factories"; +import { mockImplicitAccount, mockMultisigAccount } from "../../../../mocks/factories"; import { render, screen } from "../../../../mocks/testUtils"; import MultisigSignerTile from "./MultisigSignerTile"; import store from "../../../../utils/redux/store"; import accountsSlice from "../../../../utils/redux/slices/accountsSlice"; +import { pendingOps } from "../../../../mocks/multisig"; const { add } = accountsSlice.actions; +const signer = mockImplicitAccount(0); describe("", () => { + beforeEach(() => { + store.dispatch(add([signer])); + }); + it("should display a button for non-pending operation with signer included in the account", () => { - const account = mockImplicitAccount(0); - store.dispatch(add([account])); render( {}} + operation={pendingOps[0]} + account={mockMultisigAccount(0)} + openSignModal={_ => {}} /> ); expect(screen.getByTestId("multisig-signer-button")).toBeInTheDocument(); }); it("should hide button for pending operation with signers included in the account that already approved", () => { - const account = mockImplicitAccount(0); - store.dispatch(add([account])); render( {}} + operation={{ ...pendingOps[0], approvals: [signer.address] }} + account={mockMultisigAccount(0)} + openSignModal={_ => {}} /> ); expect(screen.queryByTestId("multisig-signer-button")).not.toBeInTheDocument(); }); it("should hide button for operation with signers not in the account", () => { - const account = mockImplicitAccount(0); + const account = { ...mockMultisigAccount(0), signers: [mockImplicitAccount(1).address] }; render( {}} + operation={pendingOps[0]} + account={account} + openSignModal={_ => {}} /> ); expect(screen.queryByTestId("multisig-signer-button")).not.toBeInTheDocument(); diff --git a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigSignerTile.tsx b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigSignerTile.tsx index 1e46c664d..ca91713d3 100644 --- a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigSignerTile.tsx +++ b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigSignerTile.tsx @@ -1,22 +1,24 @@ import { Box, Flex, Text, Heading } from "@chakra-ui/react"; import React from "react"; import colors from "../../../../style/colors"; +import { MultisigAccount } from "../../../../types/Account"; import { ImplicitAddress } from "../../../../types/Address"; import { formatPkh } from "../../../../utils/format"; import { useGetImplicitAccount } from "../../../../utils/hooks/accountHooks"; import { useGetContactName } from "../../../../utils/hooks/contactsHooks"; -import { ApproveOrExecute } from "../../../../utils/tezos/types"; +import { MultisigOperation } from "../../../../utils/multisig/types"; +import { ParamsWithFee } from "../../../ApproveExecuteForm/types"; import { Identicon } from "../../../Identicon"; import MultisigActionButton from "./MultisigActionButton"; const MultisigSignerTile: React.FC<{ - signer: ImplicitAddress; // TODO: change to ImplicitAccount - approvers: ImplicitAddress[]; // TODO: change to ImplicitAccount[] + signerAddress: ImplicitAddress; pendingApprovals: number; - onClickApproveOrExecute: (a: ApproveOrExecute) => void; - isLoading?: boolean; + operation: MultisigOperation; + account: MultisigAccount; + openSignModal: (params: ParamsWithFee) => void; }> = props => { - const signer = props.signer; + const signer = props.signerAddress; const getContactName = useGetContactName(); const getImplicitAccount = useGetImplicitAccount(); const accountLabel = getImplicitAccount(signer.pkh)?.label; diff --git a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigWithPendingOperations.test.tsx b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigWithPendingOperations.test.tsx index 9d6faf1a8..cc717e78d 100644 --- a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigWithPendingOperations.test.tsx +++ b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigWithPendingOperations.test.tsx @@ -6,7 +6,7 @@ import { pendingOps } from "../../../../mocks/multisig"; import { fireEvent, render, screen, within } from "../../../../mocks/testUtils"; import { ImplicitAccount } from "../../../../types/Account"; import { parseContractPkh, parseImplicitPkh } from "../../../../types/Address"; -import { useGetSk } from "../../../../utils/hooks/accountUtils"; +import { useGetSecretKey } from "../../../../utils/hooks/accountUtils"; import { multisigToAccount } from "../../../../utils/multisig/helpers"; import { Multisig } from "../../../../utils/multisig/types"; import accountsSlice from "../../../../utils/redux/slices/accountsSlice"; @@ -16,7 +16,7 @@ import store from "../../../../utils/redux/store"; jest.mock("../../../../utils/hooks/accountUtils"); beforeEach(() => { - (useGetSk as jest.Mock).mockReturnValue(() => Promise.resolve("mockkey")); + (useGetSecretKey as jest.Mock).mockReturnValue(() => Promise.resolve("mockkey")); }); const multisigAccount = mockMultisigAccount(0); diff --git a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/index.tsx b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/index.tsx index 66d459ac8..8c10d966e 100644 --- a/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/index.tsx +++ b/src/components/AccountCard/AssetsPannel/MultisigPendingAccordion/index.tsx @@ -21,9 +21,7 @@ export const MultisigPendingAccordion: React.FC<{ ))} diff --git a/src/components/AccountCard/ApproveExecuteForm/ApproveExecuteForm.tsx b/src/components/ApproveExecuteForm/ApproveExecuteForm.tsx similarity index 55% rename from src/components/AccountCard/ApproveExecuteForm/ApproveExecuteForm.tsx rename to src/components/ApproveExecuteForm/ApproveExecuteForm.tsx index 2c8b7d753..3fe1e01bc 100644 --- a/src/components/AccountCard/ApproveExecuteForm/ApproveExecuteForm.tsx +++ b/src/components/ApproveExecuteForm/ApproveExecuteForm.tsx @@ -1,30 +1,18 @@ import React from "react"; -import { useGetImplicitAccount } from "../../../utils/hooks/accountHooks"; -import { useSelectedNetwork } from "../../../utils/hooks/assetsHooks"; -import { SuccessStep } from "../../sendForm/steps/SuccessStep"; -import { useStepHistory } from "../../useStepHistory"; +import { useSelectedNetwork } from "../../utils/hooks/assetsHooks"; +import { SuccessStep } from "../sendForm/steps/SuccessStep"; +import { useStepHistory } from "../useStepHistory"; import { SubmitApproveOrExecuteForm } from "./SubmitApproveExecute"; import { ParamsWithFee } from "./types"; -type Steps = - | { - type: "submit"; - } - | { type: "success"; hash: string }; +type Steps = { type: "submit" } | { type: "success"; hash: string }; const ApproveExecuteForm: React.FC<{ params: ParamsWithFee }> = ({ params }) => { const history = useStepHistory({ type: "submit" }); - const getAccount = useGetImplicitAccount(); const network = useSelectedNetwork(); if (history.currentStep.type === "submit") { - const account = getAccount(params.signer.pkh); - - if (!account) { - throw new Error("Account not found"); - } - return ( = ({ params }) => onSuccess={hash => { history.goToStep({ type: "success", hash }); }} - signerAccount={account} + signerAccount={params.signer} /> ); } diff --git a/src/components/AccountCard/ApproveExecuteForm/SubmitApproveExecute.tsx b/src/components/ApproveExecuteForm/SubmitApproveExecute.tsx similarity index 66% rename from src/components/AccountCard/ApproveExecuteForm/SubmitApproveExecute.tsx rename to src/components/ApproveExecuteForm/SubmitApproveExecute.tsx index 3d9370216..3fa6eb461 100644 --- a/src/components/AccountCard/ApproveExecuteForm/SubmitApproveExecute.tsx +++ b/src/components/ApproveExecuteForm/SubmitApproveExecute.tsx @@ -11,16 +11,16 @@ import { Flex, useToast, } from "@chakra-ui/react"; -import SignButton from "../../sendForm/components/SignButton"; -import { ImplicitAccount } from "../../../types/Account"; -import { SignerConfig } from "../../../types/SignerConfig"; +import SignButton from "../sendForm/components/SignButton"; +import { ImplicitAccount } from "../../types/Account"; import { ParamsWithFee } from "./types"; -import { prettyTezAmount } from "../../../utils/format"; -import MultisigDecodedOperations from "../AssetsPannel/MultisigPendingAccordion/MultisigDecodedOperations"; -import { approveOrExecuteMultisigOperation } from "../../../utils/tezos"; -import { AccountSmallTile } from "../../AccountSelector/AccountSmallTile"; -import { ApproveOrExecute } from "../../../utils/tezos/types"; -import { TezosNetwork } from "../../../types/TezosNetwork"; +import { prettyTezAmount } from "../../utils/format"; +import MultisigDecodedOperations from "../AccountCard/AssetsPannel/MultisigPendingAccordion/MultisigDecodedOperations"; +import { approveOrExecuteMultisigOperation } from "../../utils/tezos"; +import { AccountSmallTile } from "../AccountSelector/AccountSmallTile"; +import { ApproveOrExecute } from "../../utils/tezos/types"; +import { TezosNetwork } from "../../types/TezosNetwork"; +import { TezosToolkit } from "@taquito/taquito"; type Props = { signerAccount: ImplicitAccount; @@ -41,10 +41,8 @@ export const SubmitApproveOrExecuteForm: React.FC = ({ params, }) => { const toast = useToast(); - const [isLoading, setIsLoading] = React.useState(false); - const approveOrExecute = async (config: SignerConfig) => { - setIsLoading(true); + const approveOrExecute = async (tezosToolkit: TezosToolkit) => { try { const result = await approveOrExecuteMultisigOperation( { @@ -52,14 +50,13 @@ export const SubmitApproveOrExecuteForm: React.FC = ({ operationId: params.operation.id, type: params.type, }, - config + tezosToolkit ); onSuccess(result.hash); } catch (error: any) { - toast({ title: "Failed propose or execute", description: error.message }); + toast({ title: "Failed propose or execute", description: error.message, status: "error" }); console.warn("Failed propose or execute", error); } - setIsLoading(false); }; return ( @@ -88,12 +85,7 @@ export const SubmitApproveOrExecuteForm: React.FC = ({ - + ); diff --git a/src/components/ApproveExecuteForm/types.tsx b/src/components/ApproveExecuteForm/types.tsx new file mode 100644 index 000000000..6134f21e9 --- /dev/null +++ b/src/components/ApproveExecuteForm/types.tsx @@ -0,0 +1,13 @@ +import { ApproveOrExecute } from "../../utils/tezos/types"; +import { MultisigOperation } from "../../utils/multisig/types"; +import { ContractAddress } from "../../types/Address"; +import { ImplicitAccount } from "../../types/Account"; + +export type ParamsWithFee = ApproveExecuteParams & { suggestedFeeMutez: number }; + +export type ApproveExecuteParams = { + type: ApproveOrExecute; + operation: MultisigOperation; + signer: ImplicitAccount; + multisigAddress: ContractAddress; +}; diff --git a/src/components/CSVFileUploader/CSVFileUploadForm.tsx b/src/components/CSVFileUploader/CSVFileUploadForm.tsx index 5b3c640a7..8b50f5001 100644 --- a/src/components/CSVFileUploader/CSVFileUploadForm.tsx +++ b/src/components/CSVFileUploader/CSVFileUploadForm.tsx @@ -15,9 +15,9 @@ import { } from "@chakra-ui/react"; import Papa, { ParseResult } from "papaparse"; import { FormProvider, useForm } from "react-hook-form"; -import { Address, parsePkh } from "../../types/Address"; +import { ImplicitAccount } from "../../types/Account"; import { RawOperation } from "../../types/RawOperation"; -import { useGetPk } from "../../utils/hooks/accountHooks"; +import { useGetImplicitAccount } from "../../utils/hooks/accountHooks"; import { useBatchIsSimulating, useClearBatch, @@ -25,7 +25,7 @@ import { } from "../../utils/hooks/assetsHooks"; import { useGetToken } from "../../utils/hooks/tokensHooks"; import { useAppDispatch } from "../../utils/redux/hooks"; -import { estimateAndUpdateBatch } from "../../utils/redux/thunks/estimateAndupdateBatch"; +import { estimateAndUpdateBatch } from "../../utils/redux/thunks/estimateAndUpdateBatch"; import { OwnedImplicitAccountsAutocomplete } from "../AddressAutocomplete"; import { parseOperation } from "./utils"; @@ -34,14 +34,15 @@ type FormFields = { file: FileList; }; +// TODO: add support for multisig const CSVFileUploadForm = ({ onClose }: { onClose: () => void }) => { const network = useSelectedNetwork(); const toast = useToast(); - const getPk = useGetPk(); const getToken = useGetToken(); const dispatch = useAppDispatch(); const isSimulating = useBatchIsSimulating(); const clearBatch = useClearBatch(); + const getAccount = useGetImplicitAccount(); const form = useForm({ mode: "onBlur", @@ -52,7 +53,7 @@ const CSVFileUploadForm = ({ onClose }: { onClose: () => void }) => { formState: { isValid, errors }, } = form; - const onCSVFileUploadComplete = async (sender: Address, rows: ParseResult) => { + const onCSVFileUploadComplete = async (sender: ImplicitAccount, rows: ParseResult) => { if (rows.errors.length > 0) { throw new Error("Error loading csv file."); } @@ -61,7 +62,7 @@ const CSVFileUploadForm = ({ onClose }: { onClose: () => void }) => { for (let i = 0; i < rows.data.length; i++) { const row = rows.data[i]; try { - operations.push(parseOperation(sender, row, getToken)); + operations.push(parseOperation(sender.address, row, getToken)); } catch (error: any) { toast({ title: "error", @@ -73,21 +74,22 @@ const CSVFileUploadForm = ({ onClose }: { onClose: () => void }) => { } try { - await dispatch(estimateAndUpdateBatch(sender.pkh, getPk(sender.pkh), operations, network)); + // TODO: add support for Multisig + await dispatch(estimateAndUpdateBatch(sender, sender, operations, network)); toast({ title: "CSV added to batch!" }); onClose(); } catch (error: any) { - clearBatch(sender.pkh); + clearBatch(sender.address.pkh); toast({ title: "Invalid transaction", description: error.message, status: "error" }); } }; const onSubmit = async ({ file, sender }: FormFields) => { - const senderAddress = parsePkh(sender); + const account = getAccount(sender); Papa.parse(file[0], { skipEmptyLines: true, - complete: (rows: ParseResult) => onCSVFileUploadComplete(senderAddress, rows), + complete: (rows: ParseResult) => onCSVFileUploadComplete(account, rows), }); }; diff --git a/src/components/sendForm/SendForm.test.tsx b/src/components/sendForm/SendForm.test.tsx index 9f3fc643e..cadecbdfc 100644 --- a/src/components/sendForm/SendForm.test.tsx +++ b/src/components/sendForm/SendForm.test.tsx @@ -23,14 +23,13 @@ import { fromRaw, TokenBalanceWithToken, } from "../../types/TokenBalance"; -import { SignerType, SkSignerConfig } from "../../types/SignerConfig"; import * as accountUtils from "../../utils/hooks/accountUtils"; import assetsSlice, { BatchItem } from "../../utils/redux/slices/assetsSlice"; import store from "../../utils/redux/store"; import { SendForm } from "./SendForm"; import { SendFormMode } from "./types"; -import { Estimate, TransactionOperation } from "@taquito/taquito"; +import { Estimate, TezosToolkit, TransactionOperation } from "@taquito/taquito"; import { BatchWalletOperation } from "@taquito/taquito/dist/types/wallet/batch-operation"; import { mock } from "jest-mock-extended"; import { fakeTezosUtils } from "../../mocks/fakeTezosUtils"; @@ -73,9 +72,11 @@ const fixture = (sender: string, mode: SendFormMode) => ( const MOCK_SK = "mockSk"; const MOCK_PKH = mockImplicitAccount(1).address.pkh; +const MOCK_TEZOS_TOOLKIT = {} as TezosToolkit; beforeEach(async () => { - fakeAccountUtils.useGetSk.mockReturnValue(() => Promise.resolve(MOCK_SK)); + fakeAccountUtils.useGetSecretKey.mockReturnValue(() => Promise.resolve(MOCK_SK)); + fakeTezosUtils.makeToolkit.mockResolvedValue(MOCK_TEZOS_TOOLKIT); document.getElementById("chakra-toast-portal")?.remove(); store.dispatch( tokensSlice.actions.addTokens({ @@ -179,7 +180,7 @@ describe("", () => { // expect(mockToast).toHaveBeenCalledWith(/Transaction added to batch/i); await waitFor(() => { expect(mockToast).toHaveBeenCalled(); - // expect(screen.getByText(/Transaction added to batch/i)).toBeTruthy(); + // expect(screen.getByText(/Transaction added to batch/i)).toBeInTheDocument(); }); await waitFor(() => { const addToBatchBtn = screen.getByRole("button", { @@ -267,7 +268,7 @@ describe("", () => { fireEvent.click(submit); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); }); expect(store.getState().assets.batches[MOCK_PKH]?.items).toEqual(mockBatchItems); }); @@ -290,17 +291,12 @@ describe("", () => { fireEvent.click(submit); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", "https://mainnet.tzkt.io/foo" ); }); - const config: SkSignerConfig = { - type: SignerType.SK, - network: TezosNetwork.MAINNET, - sk: MOCK_SK, - }; expect(fakeTezosUtils.submitBatch).toHaveBeenCalledWith( [ { @@ -310,7 +306,8 @@ describe("", () => { recipient: mockImplicitAddress(7), }, ], - config + mockImplicitAccount(1), + MOCK_TEZOS_TOOLKIT ); }); }); @@ -380,8 +377,8 @@ describe("", () => { tokenId: mockFA2.tokenId, }, ], - "tz1ikfEcj3LmsmxpcC1RMZNzBHbEmybCc43D", - "edpkuwYWCugiYG7nMnVUdopFmyc3sbMSiLqsJHTQgGtVhtSdLSw6H2", + mockImplicitAccount(2), + mockImplicitAccount(2), "mainnet" ); @@ -402,7 +399,7 @@ describe("", () => { submit.click(); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", "https://mainnet.tzkt.io/mockHash" @@ -420,11 +417,8 @@ describe("", () => { tokenId: mockFA2.tokenId, }, ], - { - network: "mainnet", - sk: "mockSk", - type: "sk", - } + mockImplicitAccount(2), + MOCK_TEZOS_TOOLKIT ); }); }); @@ -494,8 +488,8 @@ describe("", () => { tokenId: "0", }, ], - "tz1ikfEcj3LmsmxpcC1RMZNzBHbEmybCc43D", - "edpkuwYWCugiYG7nMnVUdopFmyc3sbMSiLqsJHTQgGtVhtSdLSw6H2", + mockImplicitAccount(2), + mockImplicitAccount(2), "mainnet" ); @@ -515,7 +509,7 @@ describe("", () => { submit.click(); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", "https://mainnet.tzkt.io/mockHash" @@ -533,7 +527,8 @@ describe("", () => { tokenId: "0", }, ], - { network: "mainnet", sk: "mockSk", type: "sk" } + mockImplicitAccount(2), + MOCK_TEZOS_TOOLKIT ); }); }); @@ -597,17 +592,13 @@ describe("", () => { fireEvent.click(submit); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", "https://mainnet.tzkt.io/mockHash" ); }); - const config: SkSignerConfig = { - type: SignerType.SK, - network: TezosNetwork.MAINNET, - sk: MOCK_SK, - }; + const contractAddress = nft.token?.contract?.address as string; expect(fakeTezosUtils.submitBatch).toHaveBeenCalledWith( [ @@ -620,7 +611,8 @@ describe("", () => { tokenId: nft.token?.tokenId, }, ], - config + mockImplicitAccount(1), + MOCK_TEZOS_TOOLKIT ); }); }); @@ -634,7 +626,7 @@ describe("", () => { test("it displays delegation form form", async () => { render(fixture(MOCK_PKH, { type: "delegation" })); - expect(screen.getByText(/delegate/i)).toBeTruthy(); + expect(screen.getByText(/delegate/i)).toBeInTheDocument(); const bakerInput = screen.getByTestId("real-address-input-baker"); fireEvent.change(bakerInput, { @@ -699,7 +691,7 @@ describe("", () => { fireEvent.click(googleSSOBtn); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", "https://mainnet.tzkt.io/foo" @@ -741,23 +733,22 @@ describe("", () => { }; test("It doesn't display password in SubmitStep", async () => { await fillForm(); - expect(screen.getByRole("button", { name: /sign with ledger/i })).toBeTruthy(); + expect(screen.getByRole("button", { name: /sign with ledger/i })).toBeInTheDocument(); expect(screen.queryByLabelText(/password/i)).not.toBeInTheDocument(); }); test("Clicking on submit transaction signs with ledger and shows operation submitted message", async () => { await fillForm(); - const ledgerBtn = screen.getByText(/sign with ledger/i); - fakeTezosUtils.submitBatch.mockResolvedValueOnce({ opHash: "foo", } as BatchWalletOperation); + const ledgerBtn = screen.getByText(/sign with ledger/i); fireEvent.click(ledgerBtn); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", "https://mainnet.tzkt.io/foo" @@ -828,7 +819,7 @@ describe("", () => { fireEvent.click(submit); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", "https://mainnet.tzkt.io/mockHash" @@ -877,7 +868,7 @@ describe("", () => { fireEvent.click(submit); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", "https://mainnet.tzkt.io/mockHash" @@ -946,7 +937,7 @@ describe("", () => { fireEvent.click(submit); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", "https://mainnet.tzkt.io/mockHash" diff --git a/src/components/sendForm/SendForm.tsx b/src/components/sendForm/SendForm.tsx index f4966334e..3f8c4b116 100644 --- a/src/components/sendForm/SendForm.tsx +++ b/src/components/sendForm/SendForm.tsx @@ -1,12 +1,12 @@ import { useToast } from "@chakra-ui/react"; -import { TransferParams } from "@taquito/taquito"; +import { TezosToolkit, TransferParams } from "@taquito/taquito"; import { useEffect, useRef, useState } from "react"; +import { RawPkh } from "../../types/Address"; import { Operation } from "../../types/Operation"; -import { SignerConfig } from "../../types/SignerConfig"; -import { useGetPk } from "../../utils/hooks/accountHooks"; +import { useGetImplicitAccount } from "../../utils/hooks/accountHooks"; import { useClearBatch, useSelectedNetwork } from "../../utils/hooks/assetsHooks"; import { useAppDispatch } from "../../utils/redux/hooks"; -import { estimateAndUpdateBatch } from "../../utils/redux/thunks/estimateAndupdateBatch"; +import { estimateAndUpdateBatch } from "../../utils/redux/thunks/estimateAndUpdateBatch"; import { FillStep } from "./steps/FillStep"; import { SubmitStep } from "./steps/SubmitStep"; import { SuccessStep } from "./steps/SuccessStep"; @@ -31,9 +31,9 @@ export const SendForm = ({ }) => { const network = useSelectedNetwork(); const toast = useToast(); - const getPk = useGetPk(); const dispatch = useAppDispatch(); const clearBatch = useClearBatch(); + const getAccount = useGetImplicitAccount(); const [isLoading, setIsLoading] = useState(false); @@ -54,8 +54,9 @@ export const SendForm = ({ return; } setIsLoading(true); + try { - const estimate = await makeSimulation(operations, getPk, network); + const estimate = await makeSimulation(operations, network); setTransferValues({ operations, @@ -63,17 +64,19 @@ export const SendForm = ({ }); } catch (error: any) { console.warn("Simulation Error", error); - toast({ title: "Invalid transaction", description: error.message }); + toast({ title: "Invalid transaction", description: error.message, status: "error" }); + } finally { + setIsLoading(false); } - - setIsLoading(false); }; - const addToBatch = async (operation: Operation, sender: string) => { - const pk = getPk(sender); + const addToBatch = async (operation: Operation, senderPkh: RawPkh) => { + // TODO: add support for Multisig + const sender = getAccount(senderPkh); try { - await dispatch(estimateAndUpdateBatch(sender, pk, [operation], network)); + // TODO: add support for Multisig + await dispatch(estimateAndUpdateBatch(sender, sender, [operation], network)); toast({ title: "Transaction added to batch!" }); } catch (error: any) { @@ -82,24 +85,18 @@ export const SendForm = ({ } }; - const execute = async (operations: FormOperations, config: SignerConfig) => { + const execute = async (operations: FormOperations, tezosToolkit: TezosToolkit) => { + // TODO: add support for Multisig if (isLoading) { return; } setIsLoading(true); - if (config.type === "ledger") { - toast({ - title: "Request sent to Ledger", - description: "Open the Tezos app on your Ledger and accept to sign the request", - }); - } - try { - const result = await makeTransfer(operations, config); + const result = await makeTransfer(operations, tezosToolkit); if (mode.type === "batch") { // TODO this will have to me moved in a thunk - const batchOwner = operations.signer.pkh; + const batchOwner = operations.signer.address.pkh; clearBatch(batchOwner); } setHash(result.hash); @@ -107,9 +104,9 @@ export const SendForm = ({ } catch (error: any) { console.warn("Failed to execute operation", error); toast({ title: "Error", description: error.message }); + } finally { + setIsLoading(false); } - - setIsLoading(false); }; if (hash) { @@ -119,10 +116,7 @@ export const SendForm = ({ if (transferValues) { return ( { - execute(transferValues.operations, config); - }} + onSubmit={tezosToolkit => execute(transferValues.operations, tezosToolkit)} isBatch={mode.type === "batch"} network={network} recap={transferValues} diff --git a/src/components/sendForm/components/SignButton.tsx b/src/components/sendForm/components/SignButton.tsx index 1a6341f8a..9ed686ec7 100644 --- a/src/components/sendForm/components/SignButton.tsx +++ b/src/components/sendForm/components/SignButton.tsx @@ -1,104 +1,109 @@ -import { Box, Button, FormControl, FormLabel, Input } from "@chakra-ui/react"; -import React from "react"; +import { + Box, + Button, + FormControl, + FormErrorMessage, + FormLabel, + Input, + useToast, +} from "@chakra-ui/react"; +import { TezosToolkit } from "@taquito/taquito"; +import React, { useState } from "react"; import { useForm } from "react-hook-form"; import { GoogleAuth } from "../../../GoogleAuth"; -import { ImplicitAccount, AccountType } from "../../../types/Account"; import { - LedgerSignerConfig, - SignerConfig, - SignerType, - SkSignerConfig, -} from "../../../types/SignerConfig"; + ImplicitAccount, + AccountType, + MnemonicAccount, + LedgerAccount, +} from "../../../types/Account"; import { TezosNetwork } from "../../../types/TezosNetwork"; -import { useGetSk } from "../../../utils/hooks/accountUtils"; +import { useGetSecretKey } from "../../../utils/hooks/accountUtils"; +import { makeToolkit } from "../../../utils/tezos"; const SignButton: React.FC<{ - onSubmit: (c: SignerConfig) => void; + onSubmit: (tezosToolkit: TezosToolkit) => Promise; signerAccount: ImplicitAccount; - isLoading: boolean; network: TezosNetwork; -}> = ({ signerAccount, isLoading, network, onSubmit }) => { - const { register, handleSubmit, formState } = useForm<{ password: string }>(); - - const getSk = useGetSk(); - const { isValid, isDirty } = formState; +}> = ({ signerAccount, network, onSubmit }) => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm<{ password: string }>({ mode: "onBlur" }); - const type = signerAccount.type; - const isGoogleSSO = type === AccountType.SOCIAL; - const isLedger = type === AccountType.LEDGER; - const isMnemonic = type === AccountType.MNEMONIC; + const getSecretKey = useGetSecretKey(); + const toast = useToast(); + const [isLoading, setIsLoading] = useState(false); - const onSubmitGoogleSSO = (sk: string) => { - if (signerAccount.type !== AccountType.SOCIAL) { - throw new Error(`Wrong signing method called`); + // wrapper function that handles changing the isLoading state & error handling + const handleSign = async (getToolkit: () => Promise) => { + if (isLoading) { + return; } - const config: SkSignerConfig = { - sk, - network, - type: SignerType.SK, - }; - onSubmit(config); - }; - - const onSubmitLedger = () => { - if (signerAccount.type !== AccountType.LEDGER) { - throw new Error(`Wrong signing method called`); - } + setIsLoading(true); - const config: LedgerSignerConfig = { - network, - derivationPath: signerAccount.derivationPath, - derivationType: signerAccount.curve, - type: SignerType.LEDGER, - }; + getToolkit() + .then(onSubmit) + .catch((error: any) => toast({ title: "Error", description: error.message, status: "error" })) + .finally(() => setIsLoading(false)); + }; - onSubmit(config); + const onMnemonicSign = async ({ password }: { password: string }) => { + return handleSign(async () => { + const secretKey = await getSecretKey(signerAccount as MnemonicAccount, password); + return makeToolkit({ type: "mnemonic", secretKey, network }); + }); }; - const onSubmitNominal = async ({ password }: { password: string }) => { - if (signerAccount.type !== AccountType.MNEMONIC) { - throw new Error(`Wrong signing method called`); - } + const onSocialSign = async (secretKey: string) => { + return handleSign(() => makeToolkit({ type: "social", secretKey, network })); + }; - // TODO disabled submit button since it"s loading - const sk = await getSk(signerAccount, password); - const config: SkSignerConfig = { - sk, - network, - type: SignerType.SK, - }; + const onLedgerSign = () => + handleSign(() => + makeToolkit({ + type: "ledger", + account: signerAccount as LedgerAccount, + network, + }) + ); - onSubmit(config); - }; return ( - - {isMnemonic ? ( - - Password: - - - ) : null} - {isGoogleSSO ? ( - - ) : ( - + + )} + {signerAccount.type === AccountType.SOCIAL && } + {signerAccount.type === AccountType.LEDGER && ( + )} diff --git a/src/components/sendForm/steps/FillStep.tsx b/src/components/sendForm/steps/FillStep.tsx index ab686c81a..4585ab2e9 100644 --- a/src/components/sendForm/steps/FillStep.tsx +++ b/src/components/sendForm/steps/FillStep.tsx @@ -21,13 +21,15 @@ import { import { TransferParams } from "@taquito/taquito"; import React from "react"; import { FormProvider, useForm, useFormContext } from "react-hook-form"; -import { AccountType, MultisigAccount } from "../../../types/Account"; -import { parseContractPkh, parseImplicitPkh, parsePkh } from "../../../types/Address"; +import { AccountType, ImplicitAccount, MultisigAccount } from "../../../types/Account"; +import { parseImplicitPkh, parsePkh, RawPkh } from "../../../types/Address"; import { getRealAmount, tokenSymbol } from "../../../types/TokenBalance"; import { Delegation, Operation } from "../../../types/Operation"; import { tezToMutez } from "../../../utils/format"; import { useAccountIsMultisig, + useGetImplicitAccount, + useGetMultisigAccount, useGetOwnedAccount, useMultisigAccounts, } from "../../../utils/hooks/accountHooks"; @@ -343,47 +345,62 @@ export const SendTezOrNFTForm = ({ }; const buildTezFromFormValues = ( - v: FormValues, + formValues: FormValues, + getImplicitAccount: (pkh: RawPkh) => ImplicitAccount, + getMultisigAccount: (pkh: RawPkh) => MultisigAccount, parameter?: TransferParams["parameter"] ): FormOperations => { const value: Operation[] = [ { type: "tez", - amount: tezToMutez(v.amount).toString(), - recipient: parsePkh(v.recipient), + amount: tezToMutez(formValues.amount).toString(), + recipient: parsePkh(formValues.recipient), parameter, }, ]; - if (v.proposalSigner !== undefined) { + if (formValues.proposalSigner !== undefined) { return { type: "proposal", - signer: parseImplicitPkh(v.proposalSigner), + signer: getImplicitAccount(formValues.proposalSigner), content: value, - sender: parseContractPkh(v.sender), + sender: getMultisigAccount(formValues.sender), }; } - return { type: "implicit", content: value, signer: parseImplicitPkh(v.sender) }; + return { + type: "implicit", + content: value, + signer: getImplicitAccount(formValues.sender), + }; }; -const buildTokenFromFormValues = (v: FormValues, asset: Token): FormOperations => { +const buildTokenFromFormValues = ( + formValues: FormValues, + asset: Token, + getImplicitAccount: (pkh: RawPkh) => ImplicitAccount, + getMultisigAccount: (pkh: RawPkh) => MultisigAccount +): FormOperations => { const token = [ toOperation(asset, { - amount: getRealAmount(asset, v.amount).toString(), - sender: v.sender, - recipient: v.recipient, + amount: getRealAmount(asset, formValues.amount).toString(), + sender: formValues.sender, + recipient: formValues.recipient, }), ]; - if (v.proposalSigner !== undefined) { + if (formValues.proposalSigner !== undefined) { return { type: "proposal", - signer: parseImplicitPkh(v.proposalSigner), + signer: getImplicitAccount(formValues.proposalSigner), content: token, - sender: parseContractPkh(v.sender), + sender: getMultisigAccount(formValues.sender), }; } - return { type: "implicit", content: token, signer: parseImplicitPkh(v.sender) }; + return { + type: "implicit", + content: token, + signer: getImplicitAccount(formValues.sender), + }; }; export const FillStep: React.FC<{ @@ -396,6 +413,9 @@ export const FillStep: React.FC<{ parameter?: TransferParams["parameter"]; mode: SendFormMode; }> = ({ onSubmit, isLoading, sender, recipient, amount, parameter, mode, onSubmitBatch }) => { + const getImplicitAccount = useGetImplicitAccount(); + const getMultisigAccount = useGetMultisigAccount(); + switch (mode.type) { case "delegation": return ( @@ -404,16 +424,17 @@ export const FillStep: React.FC<{ recipient={recipient} undelegate={mode.data?.undelegate} isLoading={isLoading} - onSubmit={v => { + onSubmit={formValues => { const delegation: Delegation = { type: "delegation", - recipient: v.baker !== undefined ? parseImplicitPkh(v.baker) : undefined, + recipient: + formValues.baker !== undefined ? parseImplicitPkh(formValues.baker) : undefined, }; onSubmit({ type: "implicit", content: [delegation], - signer: parseImplicitPkh(v.sender), + signer: getImplicitAccount(formValues.sender), }); }} /> @@ -437,8 +458,10 @@ export const FillStep: React.FC<{ v.sender ); }} - onSubmit={v => { - onSubmit(buildTezFromFormValues(v, parameter)); + onSubmit={formValues => { + onSubmit( + buildTezFromFormValues(formValues, getImplicitAccount, getMultisigAccount, parameter) + ); }} /> ); @@ -460,8 +483,15 @@ export const FillStep: React.FC<{ v.sender ); }} - onSubmit={v => { - onSubmit(buildTokenFromFormValues(v, mode.data)); + onSubmit={formValues => { + onSubmit( + buildTokenFromFormValues( + formValues, + mode.data, + getImplicitAccount, + getMultisigAccount + ) + ); }} token={mode.data} /> @@ -478,7 +508,7 @@ export const FillStep: React.FC<{ onSubmit({ type: "implicit", content: mode.data.batch, - signer: parseImplicitPkh(mode.data.signer), + signer: getImplicitAccount(mode.data.signer), }); }} /> diff --git a/src/components/sendForm/steps/SubmitStep.tsx b/src/components/sendForm/steps/SubmitStep.tsx index eb1659680..20ba13d40 100644 --- a/src/components/sendForm/steps/SubmitStep.tsx +++ b/src/components/sendForm/steps/SubmitStep.tsx @@ -10,12 +10,10 @@ import { ModalHeader, Text, } from "@chakra-ui/react"; +import { TezosToolkit } from "@taquito/taquito"; import BigNumber from "bignumber.js"; -import { AccountType } from "../../../types/Account"; import { Operation } from "../../../types/Operation"; -import { SignerConfig } from "../../../types/SignerConfig"; import { TezosNetwork } from "../../../types/TezosNetwork"; -import { useGetOwnedAccount } from "../../../utils/hooks/accountHooks"; import { useGetToken } from "../../../utils/hooks/tokensHooks"; import { getBatchSubtotal } from "../../../views/batch/batchUtils"; import { useRenderBakerSmallTile } from "../../../views/delegations/BakerSmallTile"; @@ -60,30 +58,15 @@ const NonBatchRecap = ({ transfer }: { transfer: Operation }) => { ); }; -const useGetImplicitAccount = () => { - const getAccount = useGetOwnedAccount(); - return (pkh: string) => { - const account = getAccount(pkh); - if (account.type === AccountType.MULTISIG) { - throw Error(`Account ${pkh} is not implicit`); - } - - return account; - }; -}; - export const SubmitStep: React.FC<{ network: TezosNetwork; recap: EstimatedOperation; isBatch: boolean; - onSubmit: (signerConfig: SignerConfig) => void; - isLoading: boolean; -}> = ({ recap: { fee, operations }, network, isBatch, onSubmit, isLoading }) => { + onSubmit: (tezosToolkit: TezosToolkit) => Promise; +}> = ({ recap: { fee, operations }, network, isBatch, onSubmit }) => { const feeNum = new BigNumber(fee); - const getAccount = useGetImplicitAccount(); const transfer = operations.content; - const signerAccount = getAccount(operations.signer.pkh); const total = feeNum.plus(getBatchSubtotal(transfer)); @@ -99,7 +82,7 @@ export const SubmitStep: React.FC<{ From: - + {isBatch ? ( @@ -112,12 +95,7 @@ export const SubmitStep: React.FC<{ - + diff --git a/src/components/sendForm/types.ts b/src/components/sendForm/types.ts index a0b7519bd..e6a966a6e 100644 --- a/src/components/sendForm/types.ts +++ b/src/components/sendForm/types.ts @@ -1,4 +1,5 @@ -import { ContractAddress, ImplicitAddress, parseContractPkh, parsePkh } from "../../types/Address"; +import { ImplicitAccount, MultisigAccount } from "../../types/Account"; +import { parseContractPkh, parsePkh } from "../../types/Address"; import { FA12Operation, FA2Operation, Operation } from "../../types/Operation"; import { Token } from "../../types/Token"; @@ -29,14 +30,14 @@ export type SendFormMode = TezMode | TokenMode | DelegationMode | BatchMode; export type ProposalOperations = { type: "proposal"; content: Operation[]; - sender: ContractAddress; - signer: ImplicitAddress; + sender: MultisigAccount; + signer: ImplicitAccount; }; export type ImplicitOperations = { type: "implicit"; content: Operation[]; - signer: ImplicitAddress; + signer: ImplicitAccount; }; export type FormOperations = ProposalOperations | ImplicitOperations; diff --git a/src/components/sendForm/util/execution.ts b/src/components/sendForm/util/execution.ts index 4898da316..4f2c75bde 100644 --- a/src/components/sendForm/util/execution.ts +++ b/src/components/sendForm/util/execution.ts @@ -1,36 +1,35 @@ +import { TezosToolkit } from "@taquito/taquito"; import { makeBatchLambda } from "../../../multisig/multisigUtils"; -import { parseContractPkh } from "../../../types/Address"; +import { ImplicitAccount, MultisigAccount } from "../../../types/Account"; import { Operation } from "../../../types/Operation"; -import { SignerConfig } from "../../../types/SignerConfig"; import { proposeMultisigLambda, submitBatch } from "../../../utils/tezos"; import { FormOperations } from "../types"; const makeProposeOperation = async ( operations: Operation[], - sender: string, - config: SignerConfig + sender: MultisigAccount, + tezosToolkit: TezosToolkit ) => { const lambdaActions = makeBatchLambda(operations); - const contract = parseContractPkh(sender); - return proposeMultisigLambda({ contract, lambdaActions }, config); + return proposeMultisigLambda({ contract: sender.address, lambdaActions }, tezosToolkit); }; -const makeTransferImplicit = async (operations: Operation[], config: SignerConfig) => { - return submitBatch(operations, config).then(res => { - return { - hash: res.opHash, - }; - }); +const makeTransferImplicit = async ( + operations: Operation[], + sender: ImplicitAccount, + tezosToolkit: TezosToolkit +) => { + return submitBatch(operations, sender, tezosToolkit).then(({ opHash }) => ({ hash: opHash })); }; -export const makeTransfer = (op: FormOperations, config: SignerConfig) => { +export const makeTransfer = (op: FormOperations, tezosToolkit: TezosToolkit) => { const transferToDisplay = op.content; const transfer = op.type === "proposal" - ? makeProposeOperation(transferToDisplay, op.sender.pkh, config) - : makeTransferImplicit(transferToDisplay, config); + ? makeProposeOperation(transferToDisplay, op.sender, tezosToolkit) + : makeTransferImplicit(transferToDisplay, op.signer, tezosToolkit); return transfer; }; diff --git a/src/components/sendForm/util/simulation.ts b/src/components/sendForm/util/simulation.ts index 404d7f981..9cb2d0a0d 100644 --- a/src/components/sendForm/util/simulation.ts +++ b/src/components/sendForm/util/simulation.ts @@ -1,6 +1,6 @@ import { Estimate } from "@taquito/taquito"; import { makeBatchLambda } from "../../../multisig/multisigUtils"; -import { parseContractPkh } from "../../../types/Address"; +import { ImplicitAccount } from "../../../types/Account"; import { TezosNetwork } from "../../../types/TezosNetwork"; import { estimateBatch, estimateMultisigPropose } from "../../../utils/tezos"; import { sumEstimations } from "../../../views/batch/batchUtils"; @@ -9,41 +9,32 @@ import { FormOperations, ProposalOperations } from "../types"; const makeMultisigProposalSimulation = async ( operation: ProposalOperations, network: TezosNetwork, - getPk: (pkh: string) => string + signer: ImplicitAccount ) => { const content = operation.content; - const signerPk = getPk(operation.signer.pkh); - const signerPkh = operation.signer; - const multisigContract = parseContractPkh(operation.sender.pkh); const lambdaActions = makeBatchLambda(content); const result = await estimateMultisigPropose( { lambdaActions, - contract: multisigContract, + contract: operation.sender.address, }, - signerPk, - signerPkh.pkh, + signer, network ); return result; }; +// TODO: uncouple and split into two inlined functions const getTotalFee = (estimate: Estimate[] | Estimate) => String(Array.isArray(estimate) ? sumEstimations(estimate) : estimate.suggestedFeeMutez); -export const makeSimulation = async ( - operation: FormOperations, - getPk: (pkh: string) => string, - network: TezosNetwork -) => { +export const makeSimulation = async (operation: FormOperations, network: TezosNetwork) => { if (operation.type === "proposal") { - return makeMultisigProposalSimulation(operation, network, getPk).then(getTotalFee); + return makeMultisigProposalSimulation(operation, network, operation.signer).then(getTotalFee); } const implicitOps = operation.content; - const sender = operation.signer; - const pk = getPk(sender.pkh); - return estimateBatch(implicitOps, sender.pkh, pk, network).then(getTotalFee); + return estimateBatch(implicitOps, operation.signer, operation.signer, network).then(getTotalFee); }; diff --git a/src/components/AccountCard/ApproveExecuteForm/useModal.tsx b/src/components/useModal.tsx similarity index 94% rename from src/components/AccountCard/ApproveExecuteForm/useModal.tsx rename to src/components/useModal.tsx index 6423a7020..211cde155 100644 --- a/src/components/AccountCard/ApproveExecuteForm/useModal.tsx +++ b/src/components/useModal.tsx @@ -3,6 +3,7 @@ import { useRef } from "react"; export function useModal(Component: React.ComponentType<{ params: T }>) { const { isOpen, onOpen, onClose } = useDisclosure(); + // TODO: convert to useState const paramsRef = useRef(undefined); return { diff --git a/src/integration/tezos.integration.test.ts b/src/integration/tezos.integration.test.ts index e3f6fc3c7..9fa6a1ef7 100644 --- a/src/integration/tezos.integration.test.ts +++ b/src/integration/tezos.integration.test.ts @@ -1,4 +1,5 @@ import { devPublicKeys0, devPublicKeys1 } from "../mocks/devSignerKeys"; +import { mockImplicitAccount } from "../mocks/factories"; import { ghostFA12, ghostFA2, ghostTezzard } from "../mocks/tokens"; import { parseContractPkh, parseImplicitPkh } from "../types/Address"; import { Operation } from "../types/Operation"; @@ -11,6 +12,7 @@ jest.unmock("../utils/tezos"); const pk0 = devPublicKeys0.pk; const pkh0 = parseImplicitPkh(devPublicKeys0.pkh); const pkh1 = parseImplicitPkh(devPublicKeys1.pkh); +const sender = { ...mockImplicitAccount(0), pk: pk0, address: pkh0 }; describe("Tezos utils", () => { describe("Batch", () => { @@ -23,7 +25,6 @@ describe("Tezos utils", () => { }, { type: "tez", - amount: "2", recipient: pkh1, parameter: { @@ -63,8 +64,7 @@ describe("Tezos utils", () => { tokenId: ghostFA2.tokenId, }, ]; - - const result = await operationsToBatchParams(input, pk0, pkh0.pkh, TezosNetwork.GHOSTNET); + const result = await operationsToBatchParams(input, sender); expect(result).toEqual([ { amount: 3, @@ -152,7 +152,6 @@ describe("Tezos utils", () => { storageLimit: undefined, to: ghostFA12.contract, }, - { amount: 0, fee: undefined, @@ -185,7 +184,6 @@ describe("Tezos utils", () => { }, ]); }); - describe("Estimations", () => { test("Batch estimation works with batches containg tez, FA1.2 and FA2 tokens on ghostnet", async () => { const ghostnetResult = await estimateBatch( @@ -220,16 +218,14 @@ describe("Tezos utils", () => { tokenId: ghostFA2.tokenId, }, ], - pkh0.pkh, - pk0, + sender, + sender, TezosNetwork.GHOSTNET ); - for (let i = 0; i < ghostnetResult.length; i += 1) { expect(ghostnetResult[i]).toHaveProperty("suggestedFeeMutez"); } }); - test("Batch estimation works with batches containg tez on mainnet", async () => { const mainnetResult = await estimateBatch( [ @@ -244,17 +240,14 @@ describe("Tezos utils", () => { recipient: pkh1, }, ], - pkh0.pkh, - pk0, + sender, + sender, TezosNetwork.MAINNET ); - expect(mainnetResult).toHaveLength(2); - expect(mainnetResult[0]).toHaveProperty("suggestedFeeMutez"); expect(mainnetResult[1]).toHaveProperty("suggestedFeeMutez"); }); - test("Batch estimation works with batches containing delegations on mainnet", async () => { const mainnetResult = await estimateBatch( [ @@ -263,16 +256,13 @@ describe("Tezos utils", () => { recipient: parseImplicitPkh("tz1fXRwGcgoz81Fsksx9L2rVD5wE6CpTMkLz"), }, ], - pkh0.pkh, - pk0, + sender, + sender, TezosNetwork.MAINNET ); - expect(mainnetResult).toHaveLength(1); - expect(mainnetResult[0]).toHaveProperty("suggestedFeeMutez"); }); - test("Batch estimation fails with insuficient funds on mainnet", async () => { const estimation = estimateBatch( [ @@ -283,15 +273,13 @@ describe("Tezos utils", () => { }, { type: "delegation", - recipient: parseImplicitPkh("tz1fXRwGcgoz81Fsksx9L2rVD5wE6CpTMkLz"), }, ], - pkh0.pkh, - pk0, + sender, + sender, TezosNetwork.MAINNET ); - await expect(estimation).rejects.toThrow(/tez.subtraction_underflow/i); }); }); diff --git a/src/mocks/devSignerKeys.ts b/src/mocks/devSignerKeys.ts index eec3e1793..08a09458a 100644 --- a/src/mocks/devSignerKeys.ts +++ b/src/mocks/devSignerKeys.ts @@ -1,9 +1,8 @@ import { InMemorySigner } from "@taquito/signer"; import { TezosToolkit } from "@taquito/taquito"; -import { SignerType } from "../types/SignerConfig"; import { TezosNetwork } from "../types/TezosNetwork"; import { getDefaultDerivationPath } from "../utils/account/derivationPathUtils"; -import { makeToolkitWithSigner } from "../utils/tezos"; +import { makeToolkit } from "../utils/tezos"; import { seedPhrase } from "./seedPhrase"; // make the default signer used in the dev mode. @@ -19,7 +18,7 @@ export const makeDefaultDevSigner = (index: number): InMemorySigner => { export const makeDefaultDevSignerKeys = async (index: number) => { const signer = makeDefaultDevSigner(index); return { - sk: await signer.secretKey(), + secretKey: await signer.secretKey(), pk: await signer.publicKey(), pkh: await signer.publicKeyHash(), }; @@ -43,11 +42,11 @@ export const devPublicKeys2 = { // Make the tezos toolkit with the default dev signer. export const makeToolkitFromDefaultDevSeed = async (index: number): Promise => { - const { sk } = await makeDefaultDevSignerKeys(index); + const { secretKey } = await makeDefaultDevSignerKeys(index); - return makeToolkitWithSigner({ - sk, - type: SignerType.SK, + return makeToolkit({ + secretKey, + type: "mnemonic", network: TezosNetwork.GHOSTNET, }); }; diff --git a/src/multisig/multisigSandbox2.integration.test.ts b/src/multisig/multisigSandbox2.integration.test.ts index 6d9bae3ca..236de42d1 100644 --- a/src/multisig/multisigSandbox2.integration.test.ts +++ b/src/multisig/multisigSandbox2.integration.test.ts @@ -1,6 +1,6 @@ import { makeDefaultDevSigner } from "../mocks/devSignerKeys"; +import { mockImplicitAccount } from "../mocks/factories"; import { parseContractPkh, parseImplicitPkh } from "../types/Address"; -import { SignerType } from "../types/SignerConfig"; import { TezosNetwork } from "../types/TezosNetwork"; import { tezToMutez } from "../utils/format"; import { getPendingOperations } from "../utils/multisig/fetch"; @@ -9,8 +9,9 @@ import { estimateMultisigApproveOrExecute, estimateMultisigPropose, getAccounts, + makeToolkit, proposeMultisigLambda, - transferMutez, + submitBatch, } from "../utils/tezos"; import { makeBatchLambda } from "./multisigUtils"; @@ -33,31 +34,40 @@ const FA2_KL2_CONTRACT = parseContractPkh("KT1XZoJ3PAidWVWRiKWESmPj64eKN7CEHuWZ" describe("multisig Sandbox", () => { test.skip("propose, approve and execute batch tez/FA transfers", async () => { const TEZ_TO_SEND = 1; - const devAccount0 = makeDefaultDevSigner(0); const devAccount1 = makeDefaultDevSigner(1); const devAccount2 = makeDefaultDevSigner(2); const devAccount2Address = parseImplicitPkh(await devAccount2.publicKeyHash()); + const devAccount0Sk = await devAccount0.secretKey(); + const devAccount1Sk = await devAccount1.secretKey(); const devAccount2Sk = await devAccount2.secretKey(); - const accountInfos = await getAccounts([devAccount2Address.pkh], TezosNetwork.GHOSTNET); const { balance: preDevAccount2TezBalance } = accountInfos[0]; - // First, devAccount2 send tez to MULTISIG_GHOSTNET_1 - const { fee } = await transferMutez( - MULTISIG_GHOSTNET_1.pkh, - tezToMutez(TEZ_TO_SEND.toString()).toNumber(), + await submitBatch( + [ + { + type: "tez", + amount: tezToMutez(TEZ_TO_SEND.toString()).toString(), + recipient: MULTISIG_GHOSTNET_1, + }, + ], { - type: SignerType.SK, - sk: devAccount2Sk, + ...mockImplicitAccount(0), + address: parseImplicitPkh(await devAccount2.publicKeyHash()), + pk: await devAccount2.publicKey(), + }, + await makeToolkit({ + type: "mnemonic", + secretKey: devAccount2Sk, network: TezosNetwork.GHOSTNET, - } + }) ); await sleep(15000); // devAccount0 propose a batch tez/FA tranfer to devAccount2 // devAccount0 is going to be in the approvers as well. - const lambdaActions = await makeBatchLambda([ + const lambdaActions = makeBatchLambda([ { type: "tez", recipient: devAccount2Address, @@ -80,27 +90,30 @@ describe("multisig Sandbox", () => { tokenId: "0", }, ]); - const proposeEstimate = await estimateMultisigPropose( { contract: MULTISIG_GHOSTNET_1, lambdaActions }, - await devAccount0.publicKey(), - await devAccount0.publicKeyHash(), + { + ...mockImplicitAccount(0), + address: parseImplicitPkh(await devAccount0.publicKeyHash()), + pk: await devAccount0.publicKey(), + }, TezosNetwork.GHOSTNET ); expect(proposeEstimate).toHaveProperty("suggestedFeeMutez"); - const proposeResponse = await proposeMultisigLambda( - { contract: MULTISIG_GHOSTNET_1, lambdaActions }, { - type: SignerType.SK, + contract: MULTISIG_GHOSTNET_1, + lambdaActions, + }, + await makeToolkit({ + type: "mnemonic", + secretKey: devAccount0Sk, network: TezosNetwork.GHOSTNET, - sk: await devAccount0.secretKey(), - } + }) ); expect(proposeResponse.hash).toBeTruthy(); console.log("propose done"); await sleep(15000); - // get the operation id of the proposal. const pendingOps = await getPendingOperations( [MULTISIG_GHOSTNET_1_PENDING_OPS_BIG_MAP], @@ -110,7 +123,6 @@ describe("multisig Sandbox", () => { expect(activeOps.length).toBeGreaterThanOrEqual(1); const pendingOpKey = activeOps[activeOps.length - 1].key; expect(pendingOpKey).toBeTruthy(); - // devAccount1 approves the proposal, meeting the threshold const approveEstimate = await estimateMultisigApproveOrExecute( { @@ -118,28 +130,29 @@ describe("multisig Sandbox", () => { contract: MULTISIG_GHOSTNET_1, operationId: pendingOpKey as string, }, - await devAccount1.publicKey(), - await devAccount1.publicKeyHash(), + { + ...mockImplicitAccount(0), + address: parseImplicitPkh(await devAccount1.publicKeyHash()), + pk: await devAccount1.publicKey(), + }, TezosNetwork.GHOSTNET ); expect(approveEstimate).toHaveProperty("suggestedFeeMutez"); - const approveResponse = await approveOrExecuteMultisigOperation( { type: "approve", contract: MULTISIG_GHOSTNET_1, operationId: pendingOpKey as string, }, - { - type: SignerType.SK, + await makeToolkit({ + type: "mnemonic", + secretKey: devAccount1Sk, network: TezosNetwork.GHOSTNET, - sk: await devAccount1.secretKey(), - } + }) ); expect(approveResponse.hash).toBeTruthy(); console.log("approve done"); await sleep(15000); - // The proposal to transfer to DevAccount2 can be executed const executeEstimate = await estimateMultisigApproveOrExecute( { @@ -147,31 +160,32 @@ describe("multisig Sandbox", () => { contract: MULTISIG_GHOSTNET_1, operationId: pendingOpKey as string, }, - await devAccount1.publicKey(), - await devAccount1.publicKeyHash(), + { + ...mockImplicitAccount(0), + address: parseImplicitPkh(await devAccount1.publicKeyHash()), + pk: await devAccount1.publicKey(), + }, TezosNetwork.GHOSTNET ); expect(executeEstimate).toHaveProperty("suggestedFeeMutez"); - const executeResponse = await approveOrExecuteMultisigOperation( { type: "execute", contract: MULTISIG_GHOSTNET_1, operationId: pendingOpKey as string, }, - { - type: SignerType.SK, + await makeToolkit({ + type: "mnemonic", + secretKey: devAccount1Sk, network: TezosNetwork.GHOSTNET, - sk: await devAccount1.secretKey(), - } + }) ); expect(executeResponse.hash).toBeTruthy(); console.log("execute done"); await sleep(25000); - const accountInfosAfter = await getAccounts([devAccount2Address.pkh], TezosNetwork.GHOSTNET); const { balance: postDevAccount2TezBalance } = accountInfosAfter[0]; - - expect(postDevAccount2TezBalance + fee).toEqual(preDevAccount2TezBalance); + const AVERAGE_FEE = 500; + expect(preDevAccount2TezBalance - postDevAccount2TezBalance <= AVERAGE_FEE).toEqual(true); }); }); diff --git a/src/types/SignerConfig.ts b/src/types/SignerConfig.ts index ae4c0fa03..4ab1a169c 100644 --- a/src/types/SignerConfig.ts +++ b/src/types/SignerConfig.ts @@ -1,25 +1,9 @@ -import { Curves } from "@taquito/signer"; +import { ImplicitAccount, LedgerAccount } from "./Account"; import { TezosNetwork } from "./TezosNetwork"; -export enum SignerType { - SK = "sk", - LEDGER = "ledger", -} - -type BaseSignerConfig = { - type: SignerType; - network: TezosNetwork; -}; - -export type SkSignerConfig = BaseSignerConfig & { - type: SignerType.SK; - sk: string; -}; - -export type LedgerSignerConfig = BaseSignerConfig & { - type: SignerType.LEDGER; - derivationPath: string; - derivationType: Curves; -}; - -export type SignerConfig = SkSignerConfig | LedgerSignerConfig; +export type SignerConfig = { network: TezosNetwork } & ( + | { type: "ledger"; account: LedgerAccount } + | { type: "mnemonic"; secretKey: string } + | { type: "social"; secretKey: string } + | { type: "fake"; signer: ImplicitAccount } +); diff --git a/src/utils/beacon/BeaconNotification/BeaconRequestNotification.test.tsx b/src/utils/beacon/BeaconNotification/BeaconRequestNotification.test.tsx index 8388bae15..6506407c3 100644 --- a/src/utils/beacon/BeaconNotification/BeaconRequestNotification.test.tsx +++ b/src/utils/beacon/BeaconNotification/BeaconRequestNotification.test.tsx @@ -28,7 +28,7 @@ import { walletClient } from "../beacon"; jest.mock("../../tezos"); jest.mock("../beacon"); jest.mock("../../hooks/accountUtils", () => ({ - useGetSk: () => () => "mockSk", + useGetSecretKey: () => () => "mockSk", })); const SENDER_ID = "mockSenderId"; @@ -222,7 +222,7 @@ describe("", () => { fireEvent.click(submit); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); // eslint-disable-next-line testing-library/no-wait-for-multiple-assertions expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", diff --git a/src/utils/beacon/BeaconNotification/pannels/SignPayloadRequestPanel.tsx b/src/utils/beacon/BeaconNotification/pannels/SignPayloadRequestPanel.tsx index 5efd85e08..a77adbcf8 100644 --- a/src/utils/beacon/BeaconNotification/pannels/SignPayloadRequestPanel.tsx +++ b/src/utils/beacon/BeaconNotification/pannels/SignPayloadRequestPanel.tsx @@ -11,13 +11,11 @@ import { ModalHeader, useToast, } from "@chakra-ui/react"; -import React, { useState } from "react"; +import { TezosToolkit } from "@taquito/taquito"; +import React from "react"; import SignButton from "../../../../components/sendForm/components/SignButton"; -import { AccountType } from "../../../../types/Account"; -import { SignerConfig } from "../../../../types/SignerConfig"; import { useGetImplicitAccount } from "../../../hooks/accountHooks"; import { useSelectedNetwork } from "../../../hooks/assetsHooks"; -import { makeSigner } from "../../../tezos"; import { walletClient } from "../../beacon"; const SignPayloadRequestPanel: React.FC<{ @@ -26,40 +24,26 @@ const SignPayloadRequestPanel: React.FC<{ }> = ({ request, onSuccess: onSubmit }) => { const getAccount = useGetImplicitAccount(); const network = useSelectedNetwork(); - const [isLoading, setIsLoading] = useState(false); const signerAccount = getAccount(request.sourceAddress); const toast = useToast(); if (!signerAccount) { return
"unknown account"
; } - const sign = async (config: SignerConfig) => { - setIsLoading(true); - if (signerAccount.type === AccountType.LEDGER) { - toast({ title: "connect ledger" }); - } + const sign = async (tezosToolkit: TezosToolkit) => { + const result = await tezosToolkit.signer.sign(request.payload); - try { - const signer = await makeSigner(config); + const response: SignPayloadResponseInput = { + type: BeaconMessageType.SignPayloadResponse, + id: request.id, + signingType: request.signingType, + signature: result.prefixSig, // TODO: Check if it works + }; - const result = await signer.sign(request.payload); + await walletClient.respond(response); - const response: SignPayloadResponseInput = { - type: BeaconMessageType.SignPayloadResponse, - id: request.id, - signingType: request.signingType, - signature: result.prefixSig, // TODO: Signature - }; - - await walletClient.respond(response); - - toast({ title: "Success" }); - onSubmit(); - } catch (error) { - toast({ title: "Error" }); - } - - setIsLoading(false); + toast({ title: "Successfully submitted Beacon operation", status: "success" }); + onSubmit(); }; return ( @@ -70,12 +54,7 @@ const SignPayloadRequestPanel: React.FC<{ {request.payload} - + ); diff --git a/src/utils/hooks/accountHooks.ts b/src/utils/hooks/accountHooks.ts index 6931ecfba..b3efd3579 100644 --- a/src/utils/hooks/accountHooks.ts +++ b/src/utils/hooks/accountHooks.ts @@ -5,7 +5,9 @@ import { LedgerAccount, MultisigAccount, SocialAccount, + ImplicitAccount, } from "../../types/Account"; +import { RawPkh } from "../../types/Address"; import { decrypt } from "../aes"; import { multisigToAccount } from "../multisig/helpers"; import { Multisig } from "../multisig/types"; @@ -20,9 +22,11 @@ export const useImplicitAccounts = () => { return useAppSelector(s => s.accounts.items); }; +// For cleaner code and ease of use this hook returns an ImplicitAccount +// Please make sure not to pass in non existing Pkh export const useGetImplicitAccount = () => { const accounts = useImplicitAccounts(); - return (pkh: string) => accounts.find(account => account.address.pkh === pkh); + return (pkh: RawPkh) => accounts.find(account => account.address.pkh === pkh) as ImplicitAccount; }; export const useReset = () => { @@ -136,9 +140,17 @@ export const useRemoveMnemonic = () => { export const useMultisigAccounts = (): MultisigAccount[] => { const multisigs: Multisig[] = useMultisigs(); + // TODO: use names from the store and only fallback to the random index return multisigs.map((m, i) => multisigToAccount(m, `Multisig Account ${i}`)); }; +// For cleaner code and ease of use this hook returns a MultisigAccount +// Please make sure not to pass in non existing Pkh +export const useGetMultisigAccount = () => { + const accounts = useMultisigAccounts(); + return (pkh: RawPkh) => accounts.find(account => account.address.pkh === pkh) as MultisigAccount; +}; + export const useAllAccounts = (): Account[] => { const implicit = useImplicitAccounts(); const multisig = useMultisigAccounts(); diff --git a/src/utils/hooks/accountUtils.ts b/src/utils/hooks/accountUtils.ts index 03c45ffe2..56180fc04 100644 --- a/src/utils/hooks/accountUtils.ts +++ b/src/utils/hooks/accountUtils.ts @@ -17,15 +17,22 @@ export const getTotalTezBalance = ( return filtered.reduce((acc, curr) => acc.plus(curr), new BigNumber(0)); }; -export const useGetSk = () => { +export const useGetSecretKey = () => { const seedPhrases = useAppSelector(s => s.accounts.seedPhrases); - return async (a: MnemonicAccount, password: string) => { - const encryptedMnemonic = seedPhrases[a.seedFingerPrint]; + return async (account: MnemonicAccount, password: string) => { + const encryptedMnemonic = seedPhrases[account.seedFingerPrint]; if (!encryptedMnemonic) { - throw new Error(`Missing seedphrase for account ${a.address.pkh}`); + throw new Error(`Missing seedphrase for account ${account.address.pkh}`); } - const mnemonic = await decrypt(encryptedMnemonic, password); - return deriveSkFromMnemonic(mnemonic, a.derivationPath, a.curve); + try { + const mnemonic = await decrypt(encryptedMnemonic, password); + return deriveSkFromMnemonic(mnemonic, account.derivationPath, account.curve); + } catch (error: any) { + if (error.message) { + throw error; + } + throw new Error("Failed to decrypt with the provided password"); + } }; }; diff --git a/src/utils/redux/slices/assetsSlice.test.ts b/src/utils/redux/slices/assetsSlice.test.ts index cac3c7c9b..cdd3b1473 100644 --- a/src/utils/redux/slices/assetsSlice.test.ts +++ b/src/utils/redux/slices/assetsSlice.test.ts @@ -5,14 +5,14 @@ import { waitFor } from "@testing-library/react"; import { mockDelegationTransfer, mockNftTransfer, - mockPk, mockImplicitAddress, mockTezTransaction, mockTezTransfer, mockTokenTransaction, + mockImplicitAccount, } from "../../../mocks/factories"; import accountsSlice from "./accountsSlice"; -import { estimateAndUpdateBatch } from "../thunks/estimateAndupdateBatch"; +import { estimateAndUpdateBatch } from "../thunks/estimateAndUpdateBatch"; import { estimateBatch } from "../../tezos"; import { hedgehoge } from "../../../mocks/fa12Tokens"; import { Operation } from "../../../types/Operation"; @@ -345,8 +345,8 @@ describe("Assets reducer", () => { const transfers = [mockTezTransfer(1), mockDelegationTransfer(1), mockNftTransfer(1)]; const action = estimateAndUpdateBatch( - mockImplicitAddress(1).pkh, - mockPk(1), + mockImplicitAccount(1), + mockImplicitAccount(1), transfers, TezosNetwork.MAINNET ); @@ -354,8 +354,8 @@ describe("Assets reducer", () => { store.dispatch(action); expect(estimateBatchMock).toHaveBeenCalledWith( transfers, - mockImplicitAddress(1).pkh, - mockPk(1), + mockImplicitAccount(1), + mockImplicitAccount(1), TezosNetwork.MAINNET ); expect(store.getState().assets.batches[mockImplicitAddress(1).pkh]?.isSimulating).toEqual( @@ -389,8 +389,8 @@ describe("Assets reducer", () => { const transfers = [mockTezTransfer(1)]; const action = estimateAndUpdateBatch( - mockImplicitAddress(1).pkh, - mockPk(1), + mockImplicitAccount(1), + mockImplicitAccount(1), transfers, TezosNetwork.MAINNET ); @@ -419,8 +419,8 @@ describe("Assets reducer", () => { const transfers = [mockTezTransfer(1), mockDelegationTransfer(1), mockNftTransfer(1)]; const action = estimateAndUpdateBatch( - mockImplicitAddress(1).pkh, - mockPk(1), + mockImplicitAccount(1), + mockImplicitAccount(1), transfers, TezosNetwork.MAINNET ); @@ -449,8 +449,8 @@ describe("Assets reducer", () => { const transfers = [mockTezTransfer(1), mockDelegationTransfer(1), mockNftTransfer(1)]; const action = estimateAndUpdateBatch( - mockImplicitAddress(1).pkh, - mockPk(1), + mockImplicitAccount(1), + mockImplicitAccount(1), transfers, TezosNetwork.MAINNET ); @@ -495,8 +495,8 @@ describe("Assets reducer", () => { const operations: Operation[] = []; const action = estimateAndUpdateBatch( - mockImplicitAddress(1).pkh, - mockPk(1), + mockImplicitAccount(1), + mockImplicitAccount(1), operations, TezosNetwork.MAINNET ); @@ -518,15 +518,15 @@ describe("Assets reducer", () => { store.dispatch( updateBatch({ - pkh: mockImplicitAddress(1).pkh, + pkh: mockImplicitAccount(1).address.pkh, items: [{ fee: "3", operation: mockTezTransfer(3) }], }) ); const transfers = [mockTezTransfer(1), mockDelegationTransfer(1), mockNftTransfer(1)]; const action = estimateAndUpdateBatch( - mockImplicitAddress(1).pkh, - mockPk(1), + mockImplicitAccount(1), + mockImplicitAccount(1), transfers, TezosNetwork.MAINNET ); diff --git a/src/utils/redux/slices/assetsSlice.ts b/src/utils/redux/slices/assetsSlice.ts index 74faf877e..853933423 100644 --- a/src/utils/redux/slices/assetsSlice.ts +++ b/src/utils/redux/slices/assetsSlice.ts @@ -11,6 +11,7 @@ import { Operation } from "../../../types/Operation"; export type BatchItem = { operation: Operation; fee: string }; export type Batch = { + // TODO: check if it is really needed isSimulating: boolean; items: Array; }; diff --git a/src/utils/redux/thunks/estimateAndupdateBatch.ts b/src/utils/redux/thunks/estimateAndUpdateBatch.ts similarity index 59% rename from src/utils/redux/thunks/estimateAndupdateBatch.ts rename to src/utils/redux/thunks/estimateAndUpdateBatch.ts index 959ca7c1e..5ba5833b9 100644 --- a/src/utils/redux/thunks/estimateAndupdateBatch.ts +++ b/src/utils/redux/thunks/estimateAndUpdateBatch.ts @@ -1,4 +1,5 @@ import { AnyAction, ThunkAction } from "@reduxjs/toolkit"; +import { Account, ImplicitAccount } from "../../../types/Account"; import { Operation } from "../../../types/Operation"; import { TezosNetwork } from "../../../types/TezosNetwork"; import { operationsToBatchItems } from "../../../views/batch/batchUtils"; @@ -7,8 +8,8 @@ import { RootState } from "../store"; const { updateBatch: addToBatch, batchSimulationEnd, batchSimulationStart } = assetsSlice.actions; export const estimateAndUpdateBatch = ( - pkh: string, - pk: string, + sender: Account, + signer: ImplicitAccount, operations: Operation[], network: TezosNetwork ): ThunkAction, RootState, unknown, AnyAction> => { @@ -19,19 +20,19 @@ export const estimateAndUpdateBatch = ( const batches = getState().assets.batches; - if (batches[pkh]?.isSimulating) { - throw new Error(`Simulation already ongoing for ${pkh}`); + if (batches[sender.address.pkh]?.isSimulating) { + throw new Error(`Simulation already ongoing for ${sender.address.pkh}`); } - dispatch(batchSimulationStart({ pkh })); + dispatch(batchSimulationStart({ pkh: sender.address.pkh })); try { - const items = await operationsToBatchItems(operations, pkh, pk, network); - dispatch(addToBatch({ pkh, items })); + const items = await operationsToBatchItems(operations, sender, signer, network); + dispatch(addToBatch({ pkh: sender.address.pkh, items })); } catch (error) { - dispatch(batchSimulationEnd({ pkh })); + dispatch(batchSimulationEnd({ pkh: sender.address.pkh })); throw error; } - dispatch(batchSimulationEnd({ pkh })); + dispatch(batchSimulationEnd({ pkh: sender.address.pkh })); }; }; diff --git a/src/utils/restoreAccounts.ts b/src/utils/restoreAccounts.ts index 9454e2de8..404bf3b85 100644 --- a/src/utils/restoreAccounts.ts +++ b/src/utils/restoreAccounts.ts @@ -30,20 +30,14 @@ export const restoreAccount = async ( * Use this to get SK for a mnemonic account. * Get the corresponding mnemonic via the fingerprint field */ -export const deriveSkFromMnemonic = async ( - mnemonic: string, - derivationPath: string, - curve: Curves -) => { - const signer = await InMemorySigner.fromMnemonic({ +export const deriveSkFromMnemonic = (mnemonic: string, derivationPath: string, curve: Curves) => + InMemorySigner.fromMnemonic({ mnemonic, derivationPath, curve, - }); - - return signer.secretKey(); -}; + }).secretKey(); +// TODO: fix, doesn't restore all the accounts, but only the first three export const restoreAccounts = async ( seedPhrase: string, derivationPathPattern: string, diff --git a/src/utils/tezos/estimate.ts b/src/utils/tezos/estimate.ts index 857e94c81..98e093003 100644 --- a/src/utils/tezos/estimate.ts +++ b/src/utils/tezos/estimate.ts @@ -1,49 +1,48 @@ import { Estimate } from "@taquito/taquito"; +import { Account, ImplicitAccount } from "../../types/Account"; import { Operation } from "../../types/Operation"; import { TezosNetwork } from "../../types/TezosNetwork"; import { makeMultisigApproveOrExecuteMethod, makeMultisigProposeMethod, - makeToolkitWithDummySigner, + makeToolkit, } from "./helpers"; import { operationsToBatchParams } from "./params"; import { MultisigApproveOrExecuteMethodArgs, MultisigProposeMethodArgs } from "./types"; export const estimateMultisigPropose = async ( params: MultisigProposeMethodArgs, - senderPk: string, - senderPkh: string, + signer: ImplicitAccount, network: TezosNetwork ): Promise => { - const Tezos = makeToolkitWithDummySigner(senderPk, senderPkh, network); + const tezosToolkit = await makeToolkit({ type: "fake", signer, network }); - const propseMethod = await makeMultisigProposeMethod(params, Tezos); + const propseMethod = await makeMultisigProposeMethod(params, tezosToolkit); - return Tezos.estimate.transfer(propseMethod.toTransferParams()); + return tezosToolkit.estimate.transfer(propseMethod.toTransferParams()); }; export const estimateMultisigApproveOrExecute = async ( params: MultisigApproveOrExecuteMethodArgs, - senderPk: string, - senderPkh: string, + signer: ImplicitAccount, network: TezosNetwork ): Promise => { - const Tezos = makeToolkitWithDummySigner(senderPk, senderPkh, network); + const tezosToolkit = await makeToolkit({ type: "fake", signer, network }); - const approveOrExecuteMethod = await makeMultisigApproveOrExecuteMethod(params, Tezos); + const approveOrExecuteMethod = await makeMultisigApproveOrExecuteMethod(params, tezosToolkit); - return Tezos.estimate.transfer(approveOrExecuteMethod.toTransferParams()); + return tezosToolkit.estimate.transfer(approveOrExecuteMethod.toTransferParams()); }; export const estimateBatch = async ( operations: Operation[], - pkh: string, - pk: string, + sender: Account, + signer: ImplicitAccount, network: TezosNetwork ): Promise => { - const batch = await operationsToBatchParams(operations, pk, pkh, network); + const batch = await operationsToBatchParams(operations, sender); - const Tezos = makeToolkitWithDummySigner(pk, pkh, network); + const tezosToolkit = await makeToolkit({ type: "fake", signer, network }); - return Tezos.estimate.batch(batch); + return tezosToolkit.estimate.batch(batch); }; diff --git a/src/utils/tezos/dummySigner.test.ts b/src/utils/tezos/fakeSigner.test.ts similarity index 57% rename from src/utils/tezos/dummySigner.test.ts rename to src/utils/tezos/fakeSigner.test.ts index fd670dd2b..577049575 100644 --- a/src/utils/tezos/dummySigner.test.ts +++ b/src/utils/tezos/fakeSigner.test.ts @@ -1,20 +1,20 @@ import { mockPk, mockImplicitAddress } from "../../mocks/factories"; -import { DummySigner } from "./dummySigner"; +import { FakeSigner } from "./fakeSigner"; -describe("dummySigner", () => { - test("dummySigner sets pk and pkh", async () => { - const signer = new DummySigner(mockPk(0), mockImplicitAddress(0).pkh); +describe("fakeSigner", () => { + test("fakeSigner sets pk and pkh", async () => { + const signer = new FakeSigner(mockPk(0), mockImplicitAddress(0).pkh); expect(await signer.publicKeyHash()).toEqual(mockImplicitAddress(0).pkh); expect(signer.pk).toEqual(mockPk(0)); }); test("sign method throws error", async () => { - const signer = new DummySigner(mockPk(0), mockImplicitAddress(0).pkh); + const signer = new FakeSigner(mockPk(0), mockImplicitAddress(0).pkh); await expect(signer.sign()).rejects.toThrowError("`sign` method not available"); }); test("secretKey method throws error", async () => { - const signer = new DummySigner(mockPk(0), mockImplicitAddress(0).pkh); + const signer = new FakeSigner(mockPk(0), mockImplicitAddress(0).pkh); await expect(signer.secretKey()).rejects.toThrowError("empty secret key"); }); }); diff --git a/src/utils/tezos/dummySigner.ts b/src/utils/tezos/fakeSigner.ts similarity index 92% rename from src/utils/tezos/dummySigner.ts rename to src/utils/tezos/fakeSigner.ts index fcf71d4e2..08b5e7806 100644 --- a/src/utils/tezos/dummySigner.ts +++ b/src/utils/tezos/fakeSigner.ts @@ -1,5 +1,5 @@ import { Signer } from "@taquito/taquito"; -export class DummySigner implements Signer { +export class FakeSigner implements Signer { pk: string; pkh: string; diff --git a/src/utils/tezos/helpers.test.ts b/src/utils/tezos/helpers.test.ts index 6767f9418..c0d6f83c0 100644 --- a/src/utils/tezos/helpers.test.ts +++ b/src/utils/tezos/helpers.test.ts @@ -12,7 +12,7 @@ import { } from "./helpers"; jest.mock("@taquito/signer"); jest.mock("@taquito/taquito"); -jest.mock("./dummySigner"); +jest.mock("./fakeSigner"); jest.mock("axios"); const mockedAxios = axios as jest.Mocked; diff --git a/src/utils/tezos/helpers.ts b/src/utils/tezos/helpers.ts index e3183d8a4..eaf9b105e 100644 --- a/src/utils/tezos/helpers.ts +++ b/src/utils/tezos/helpers.ts @@ -6,12 +6,12 @@ import { TezosToolkit, TransferParams } from "@taquito/taquito"; import axios from "axios"; import { shuffle } from "lodash"; import { FA12Operation, FA2Operation } from "../../types/RawOperation"; -import { SignerConfig, SignerType } from "../../types/SignerConfig"; +import { SignerConfig } from "../../types/SignerConfig"; import { TezosNetwork } from "../../types/TezosNetwork"; import { PublicKeyPair } from "../restoreAccounts"; import { RawTzktGetAddressType } from "../tzkt/types"; import { nodeUrls, tzktUrls } from "./consts"; -import { DummySigner } from "./dummySigner"; +import { FakeSigner } from "./fakeSigner"; import { MultisigApproveOrExecuteMethodArgs, MultisigProposeMethodArgs } from "./types"; export const addressExists = async ( @@ -51,16 +51,16 @@ export const curvesToDerivationPath = (curves: Curves): DerivationType => { case "p256": return DerivationType.P256; case "bip25519": - throw new Error("bip25519 is not supported in Tezos"); + throw new Error("bip25519 is not supported in Tezos"); // TODO: Verify this statement } }; export const makeSigner = async (config: SignerConfig) => { switch (config.type) { - case SignerType.SK: - return new InMemorySigner(config.sk); - - case SignerType.LEDGER: { + case "social": + case "mnemonic": + return new InMemorySigner(config.secretKey); + case "ledger": { // Close existing connections to be able to reinitiate const devices = await TransportWebHID.list(); for (let i = 0; i < devices.length; i++) { @@ -69,35 +69,22 @@ export const makeSigner = async (config: SignerConfig) => { const transport = await TransportWebHID.create(); const signer = new LedgerSigner( transport, - config.derivationPath, + config.account.derivationPath, false, // PK Verification not needed - curvesToDerivationPath(config.derivationType) + curvesToDerivationPath(config.account.curve) ); return signer; } + case "fake": + return new FakeSigner(config.signer.pk, config.signer.address.pkh); } }; -export const makeToolkitWithSigner = async (config: SignerConfig) => { - const Tezos = new TezosToolkit(nodeUrls[config.network]); +export const makeToolkit = async (config: SignerConfig) => { + const toolkit = new TezosToolkit(nodeUrls[config.network]); const signer = await makeSigner(config); - - Tezos.setProvider({ - signer, - }); - return Tezos; -}; - -export const makeToolkitWithDummySigner = ( - pk: string, - pkh: string, - network: TezosNetwork -): TezosToolkit => { - const Tezos = new TezosToolkit(nodeUrls[network]); - Tezos.setProvider({ - signer: new DummySigner(pk, pkh), - }); - return Tezos; + toolkit.setSignerProvider(signer); + return toolkit; }; export const getPkAndPkhFromSk = async (sk: string): Promise => { @@ -190,6 +177,7 @@ export const makeTokenTransferParams = ( }; }; +// TODO: convert to an offline method export const makeMultisigProposeMethod = async ( { lambdaActions, contract }: MultisigProposeMethodArgs, toolkit: TezosToolkit @@ -198,6 +186,7 @@ export const makeMultisigProposeMethod = async ( return contractInstance.methods.propose(lambdaActions); }; +// TODO: convert to an offline method export const makeMultisigApproveOrExecuteMethod = async ( { type, contract, operationId }: MultisigApproveOrExecuteMethodArgs, toolkit: TezosToolkit diff --git a/src/utils/tezos/operations.ts b/src/utils/tezos/operations.ts index aa93ee920..3443f2a23 100644 --- a/src/utils/tezos/operations.ts +++ b/src/utils/tezos/operations.ts @@ -1,53 +1,32 @@ -import { TransactionOperation, TransferParams } from "@taquito/taquito"; +import { TezosToolkit, TransactionOperation } from "@taquito/taquito"; import { BatchWalletOperation } from "@taquito/taquito/dist/types/wallet/batch-operation"; +import { Account } from "../../types/Account"; import { Operation } from "../../types/Operation"; -import { SignerConfig } from "../../types/SignerConfig"; -import { - makeMultisigApproveOrExecuteMethod, - makeMultisigProposeMethod, - makeToolkitWithSigner, -} from "./helpers"; +import { makeMultisigApproveOrExecuteMethod, makeMultisigProposeMethod } from "./helpers"; import { operationsToWalletParams } from "./params"; import { MultisigApproveOrExecuteMethodArgs, MultisigProposeMethodArgs } from "./types"; -export const transferMutez = async ( - recipient: string, - amount: number, - config: SignerConfig, - parameter?: TransferParams["parameter"] -): Promise => { - const Tezos = await makeToolkitWithSigner(config); - return Tezos.contract.transfer({ - to: recipient, - amount: amount, - parameter, - mutez: true, - }); -}; - export const proposeMultisigLambda = async ( params: MultisigProposeMethodArgs, - config: SignerConfig + tezosToolkit: TezosToolkit ): Promise => { - const Tezos = await makeToolkitWithSigner(config); - const proposeMethod = await makeMultisigProposeMethod(params, Tezos); + const proposeMethod = await makeMultisigProposeMethod(params, tezosToolkit); return proposeMethod.send(); }; export const approveOrExecuteMultisigOperation = async ( params: MultisigApproveOrExecuteMethodArgs, - config: SignerConfig + tezosToolkit: TezosToolkit ): Promise => { - const Tezos = await makeToolkitWithSigner(config); - const approveOrExecuteMethod = await makeMultisigApproveOrExecuteMethod(params, Tezos); + const approveOrExecuteMethod = await makeMultisigApproveOrExecuteMethod(params, tezosToolkit); return approveOrExecuteMethod.send(); }; export const submitBatch = async ( operation: Operation[], - config: SignerConfig + sender: Account, + tezosToolkit: TezosToolkit ): Promise => { - const Tezos = await makeToolkitWithSigner(config); - const params = await operationsToWalletParams(operation, Tezos); - return Tezos.wallet.batch(params).send(); + const params = await operationsToWalletParams(operation, sender); + return tezosToolkit.wallet.batch(params).send(); }; diff --git a/src/utils/tezos/params.ts b/src/utils/tezos/params.ts index 31d77f12a..3e339a7f9 100644 --- a/src/utils/tezos/params.ts +++ b/src/utils/tezos/params.ts @@ -1,20 +1,19 @@ -import { OpKind, ParamsWithKind, TezosToolkit, WalletParamsWithKind } from "@taquito/taquito"; +import { OpKind, ParamsWithKind, WalletParamsWithKind } from "@taquito/taquito"; +import { Account } from "../../types/Account"; import { Operation } from "../../types/Operation"; -import { TezosNetwork } from "../../types/TezosNetwork"; -import { makeTokenTransferParams, makeToolkitWithDummySigner } from "./helpers"; +import { makeTokenTransferParams } from "./helpers"; export const operationsToWalletParams = async ( operations: Operation[], - signer: TezosToolkit + sender: Account ): Promise => - operationsToParams(operations, signer) as Promise; + operationsToParams(operations, sender) as Promise; export const operationsToParams = async ( operations: Operation[], - toolkit: TezosToolkit + sender: Account ): Promise => { const result: ParamsWithKind[] = []; - const signerPkh = await toolkit.signer.publicKeyHash(); for (const operation of operations) { switch (operation.type) { @@ -30,7 +29,7 @@ export const operationsToParams = async ( case "delegation": result.push({ kind: OpKind.DELEGATION, - source: signerPkh, + source: sender.address.pkh, delegate: operation.recipient?.pkh, }); break; @@ -49,15 +48,10 @@ export const operationsToParams = async ( export const operationsToBatchParams = async ( operations: Operation[], - pk: string, - pkh: string, - network: TezosNetwork + sender: Account ): Promise => { if (!operations.length) { return []; } - - const Tezos = makeToolkitWithDummySigner(pk, pkh, network); - - return operationsToParams(operations, Tezos); + return operationsToParams(operations, sender); }; diff --git a/src/views/batch/BatchView.test.tsx b/src/views/batch/BatchView.test.tsx index 22461e667..0ad8e33fa 100644 --- a/src/views/batch/BatchView.test.tsx +++ b/src/views/batch/BatchView.test.tsx @@ -1,3 +1,4 @@ +import { TezosToolkit } from "@taquito/taquito"; import { mockImplicitAccount, mockImplicitAddress } from "../../mocks/factories"; import { fakeTezosUtils } from "../../mocks/fakeTezosUtils"; import { @@ -7,11 +8,10 @@ import { setBatchEstimationPerTransaction, } from "../../mocks/helpers"; import { act, fireEvent, render, screen, waitFor, within } from "../../mocks/testUtils"; -import { SignerType, SkSignerConfig } from "../../types/SignerConfig"; import { TezosNetwork } from "../../types/TezosNetwork"; -import { useGetSk } from "../../utils/hooks/accountUtils"; +import { useGetSecretKey } from "../../utils/hooks/accountUtils"; import store from "../../utils/redux/store"; -import { estimateAndUpdateBatch } from "../../utils/redux/thunks/estimateAndupdateBatch"; +import { estimateAndUpdateBatch } from "../../utils/redux/thunks/estimateAndUpdateBatch"; import BatchView from "./BatchView"; // These tests might take long in the CI @@ -30,7 +30,7 @@ jest.mock("@chakra-ui/react", () => { jest.mock("../../utils/hooks/accountUtils"); -const useGetSkMock = useGetSk as jest.Mock; +const useGetSecretKeyMock = useGetSecretKey as jest.Mock; const fixture = () => ; @@ -38,7 +38,7 @@ beforeEach(() => { dispatchMockAccounts([mockImplicitAccount(1), mockImplicitAccount(2), mockImplicitAccount(3)]); setBatchEstimationPerTransaction(fakeTezosUtils.estimateBatch, 10); - useGetSkMock.mockReturnValue(() => "mockSk"); + useGetSecretKeyMock.mockReturnValue(() => "mockSk"); fakeTezosUtils.submitBatch.mockResolvedValue({ opHash: "foo" } as any); }); @@ -85,61 +85,6 @@ const addItemsToBatchViaUI = async () => { closeModal(); }; -// Can run in beforeEach -const addItemsToBatchViaStore = async () => { - await store.dispatch( - estimateAndUpdateBatch( - mockImplicitAccount(1).address.pkh, - mockImplicitAccount(1).pk, - [ - { - type: "tez", - recipient: mockImplicitAddress(1), - amount: "1000000", - }, - { - type: "tez", - recipient: mockImplicitAddress(2), - amount: "2000000", - }, - { - type: "tez", - recipient: mockImplicitAddress(3), - amount: "3000000", - }, - ], - - TezosNetwork.MAINNET - ) - ); - - await store.dispatch( - estimateAndUpdateBatch( - mockImplicitAccount(2).address.pkh, - mockImplicitAccount(2).pk, - [ - { - type: "tez", - recipient: mockImplicitAddress(9), - amount: "4", - }, - { - type: "tez", - recipient: mockImplicitAddress(4), - amount: "5", - }, - { - type: "tez", - recipient: mockImplicitAddress(5), - amount: "6", - }, - ], - - TezosNetwork.MAINNET - ) - ); -}; - describe("", () => { describe("Given no batch has beed added", () => { it("a message 'no batches are present' is displayed", () => { @@ -159,9 +104,60 @@ describe("", () => { }); describe("Given batches have been added", () => { + const MOCK_TEZOS_TOOLKIT = {}; beforeEach(async () => { - // This is fast and can run before each test - await addItemsToBatchViaStore(); + await store.dispatch( + estimateAndUpdateBatch( + mockImplicitAccount(1), + mockImplicitAccount(1), + [ + { + type: "tez", + recipient: mockImplicitAddress(1), + amount: "1000000", + }, + { + type: "tez", + recipient: mockImplicitAddress(2), + amount: "2000000", + }, + { + type: "tez", + recipient: mockImplicitAddress(3), + amount: "3000000", + }, + ], + + TezosNetwork.MAINNET + ) + ); + + store.dispatch( + estimateAndUpdateBatch( + mockImplicitAccount(2), + mockImplicitAccount(2), + [ + { + type: "tez", + recipient: mockImplicitAddress(9), + amount: "4", + }, + { + type: "tez", + recipient: mockImplicitAddress(4), + amount: "5", + }, + { + type: "tez", + recipient: mockImplicitAddress(5), + amount: "6", + }, + ], + + TezosNetwork.MAINNET + ) + ); + fakeTezosUtils.makeToolkit.mockResolvedValue(MOCK_TEZOS_TOOLKIT as TezosToolkit); }); it("should display fee total and subtotal for a given batch", async () => { @@ -242,18 +238,14 @@ describe("", () => { fireEvent.click(submit); await waitFor(() => { - expect(screen.getByText(/Operation Submitted/i)).toBeTruthy(); + expect(screen.getByText(/Operation Submitted/i)).toBeInTheDocument(); }); expect(screen.getByTestId(/tzkt-link/i)).toHaveProperty( "href", "https://mainnet.tzkt.io/foo" ); - const config: SkSignerConfig = { - type: SignerType.SK, - network: TezosNetwork.MAINNET, - sk: "mockSk", - }; + expect(fakeTezosUtils.submitBatch).toHaveBeenCalledWith( [ { @@ -272,7 +264,8 @@ describe("", () => { recipient: mockImplicitAddress(3), }, ], - config + mockImplicitAccount(1), + MOCK_TEZOS_TOOLKIT ); expect( diff --git a/src/views/batch/batchUtils.ts b/src/views/batch/batchUtils.ts index 85aa2af4d..aef4928c7 100644 --- a/src/views/batch/batchUtils.ts +++ b/src/views/batch/batchUtils.ts @@ -5,6 +5,7 @@ import { zip } from "../../utils/helpers"; import { Operation } from "../../types/Operation"; import { TezosNetwork } from "../../types/TezosNetwork"; import { BatchItem } from "../../utils/redux/slices/assetsSlice"; +import { Account, ImplicitAccount } from "../../types/Account"; export const getTotalFee = (items: BatchItem[]): BigNumber => { const fee = items.reduce((acc, curr) => { @@ -36,16 +37,17 @@ export const sumEstimations = (es: Estimate[]) => { export const operationsToBatchItems = async ( operations: Operation[], - pkh: string, - pk: string, + sender: Account, + signer: ImplicitAccount, network: TezosNetwork ) => { - const estimations = await estimateBatch(operations, pkh, pk, network); - const items = zip(operations, estimations).map(([o, e]) => { + // TODO: add support for Multisig + const estimations = await estimateBatch(operations, sender, signer, network); + + return zip(operations, estimations).map(([operation, estimate]) => { return { - fee: String(e.suggestedFeeMutez), - operation: o, + fee: String(estimate.suggestedFeeMutez), + operation, }; }); - return items; }; diff --git a/src/views/home/useSendFormModal.tsx b/src/views/home/useSendFormModal.tsx index 5f34619cb..25c26fd7b 100644 --- a/src/views/home/useSendFormModal.tsx +++ b/src/views/home/useSendFormModal.tsx @@ -1,4 +1,4 @@ -import { useModal } from "../../components/AccountCard/ApproveExecuteForm/useModal"; +import { useModal } from "../../components/useModal"; import SendForm from "../../components/sendForm"; import { SendFormMode } from "../../components/sendForm/types";