From 567431d0980e91ae0a70bb16fc9ce1f1bcae7cda Mon Sep 17 00:00:00 2001 From: Oleg Chendighelean Date: Thu, 23 May 2024 17:32:03 +0300 Subject: [PATCH] Add origination operation modal --- .../SendFlow/Beacon/BeaconSignPage.tsx | 4 +- .../OriginationOperationSignModal.test.tsx | 90 ++++++++++++++ .../Beacon/OriginationOperationSignModal.tsx | 113 ++++++++++++++++++ .../beacon/useHandleBeaconMessage.test.tsx | 3 +- src/utils/beacon/useHandleBeaconMessage.tsx | 16 ++- 5 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 src/components/SendFlow/Beacon/OriginationOperationSignModal.test.tsx create mode 100644 src/components/SendFlow/Beacon/OriginationOperationSignModal.tsx diff --git a/src/components/SendFlow/Beacon/BeaconSignPage.tsx b/src/components/SendFlow/Beacon/BeaconSignPage.tsx index 8f55779371..f9cc157483 100644 --- a/src/components/SendFlow/Beacon/BeaconSignPage.tsx +++ b/src/components/SendFlow/Beacon/BeaconSignPage.tsx @@ -1,6 +1,7 @@ import { BeaconSignPageProps } from "./BeaconSignPageProps"; import { ContractCallSignPage } from "./ContractCallSignPage"; import { DelegationSignPage } from "./DelegationSignPage"; +import { OriginationOperationSignModal } from "./OriginationOperationSignModal"; import { TezSignPage as BeaconTezSignPage } from "./TezSignPage"; import { UndelegationSignPage } from "./UndelegationSignPage"; @@ -20,6 +21,8 @@ export const BeaconSignPage: React.FC = ({ operation, fee, case "undelegation": { return ; } + case "contract_origination": + return ; /** * FA1/2 are impossible to get here because we don't parse them * instead we get a generic contract call @@ -28,7 +31,6 @@ export const BeaconSignPage: React.FC = ({ operation, fee, */ case "fa1.2": case "fa2": - case "contract_origination": throw new Error("Unsupported operation type"); } }; diff --git a/src/components/SendFlow/Beacon/OriginationOperationSignModal.test.tsx b/src/components/SendFlow/Beacon/OriginationOperationSignModal.test.tsx new file mode 100644 index 0000000000..bee1d3dd68 --- /dev/null +++ b/src/components/SendFlow/Beacon/OriginationOperationSignModal.test.tsx @@ -0,0 +1,90 @@ +import { BeaconMessageType, NetworkType, OperationRequestOutput } from "@airgap/beacon-wallet"; +import type { BatchWalletOperation } from "@taquito/taquito/dist/types/wallet/batch-operation"; +import BigNumber from "bignumber.js"; + +import { OriginationOperationSignModal } from "./OriginationOperationSignModal"; +import { mockContractOrigination, mockImplicitAccount } from "../../../mocks/factories"; +import { + act, + dynamicModalContextMock, + render, + screen, + userEvent, + waitFor, +} from "../../../mocks/testUtils"; +import { WalletClient } from "../../../utils/beacon/WalletClient"; +import { prettyTezAmount } from "../../../utils/format"; +import { useGetSecretKey } from "../../../utils/hooks/getAccountDataHooks"; +import { executeOperations, makeToolkit } from "../../../utils/tezos"; +import { SuccessStep } from "../SuccessStep"; +import { GHOSTNET } from "../../../types/Network"; +import { TezosToolkit } from "@taquito/taquito"; + +const message = { + id: "messageid", + type: BeaconMessageType.OperationRequest, + network: { type: NetworkType.GHOSTNET }, + appMetadata: {}, +} as OperationRequestOutput; +const operation = { + type: "implicit" as const, + sender: mockImplicitAccount(0), + signer: mockImplicitAccount(0), + operations: [mockContractOrigination(0)], +}; +const fee = BigNumber(123); + +jest.mock("../../../utils/tezos", () => ({ + ...jest.requireActual("../../../utils/tezos"), + executeOperations: jest.fn(), + makeToolkit: jest.fn(), +})); + +jest.mock("../../../utils/hooks/getAccountDataHooks", () => ({ + ...jest.requireActual("../../../utils/hooks/getAccountDataHooks"), + useGetSecretKey: jest.fn(), +})); + +describe("", () => { + it("renders fee", () => { + render(); + + expect(screen.getByText(prettyTezAmount(fee))).toBeVisible(); + }); + + it("passes correct payload to sign handler", async () => { + const user = userEvent.setup(); + const testToolkit = "testToolkit" as unknown as TezosToolkit; + + jest.mocked(makeToolkit).mockImplementation(() => Promise.resolve(testToolkit)); + jest.mocked(useGetSecretKey).mockImplementation(() => () => Promise.resolve("secretKey")); + jest.mocked(executeOperations).mockResolvedValue({ opHash: "ophash" } as BatchWalletOperation); + jest.spyOn(WalletClient, "respond").mockResolvedValue(); + + render(); + + await act(() => user.type(screen.getByLabelText("Password"), "Password")); + + const signButton = screen.getByRole("button", { + name: "Confirm Transaction", + }); + await waitFor(() => expect(signButton).toBeEnabled()); + await act(() => user.click(signButton)); + + expect(makeToolkit).toHaveBeenCalledWith({ + type: "mnemonic", + secretKey: "secretKey", + network: GHOSTNET, + }); + expect(executeOperations).toHaveBeenCalledWith(operation, testToolkit); + + await waitFor(() => + expect(WalletClient.respond).toHaveBeenCalledWith({ + type: BeaconMessageType.OperationResponse, + id: message.id, + transactionHash: "ophash", + }) + ); + expect(dynamicModalContextMock.openWith).toHaveBeenCalledWith(); + }); +}); diff --git a/src/components/SendFlow/Beacon/OriginationOperationSignModal.tsx b/src/components/SendFlow/Beacon/OriginationOperationSignModal.tsx new file mode 100644 index 0000000000..1c3c7583c9 --- /dev/null +++ b/src/components/SendFlow/Beacon/OriginationOperationSignModal.tsx @@ -0,0 +1,113 @@ +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + AspectRatio, + Flex, + Heading, + Image, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + Text, +} from "@chakra-ui/react"; +import { capitalize } from "lodash"; + +import { BeaconSignPageProps } from "./BeaconSignPageProps"; +import { useSignWithBeacon } from "./useSignWithBeacon"; +import colors from "../../../style/colors"; +import { ContractOrigination } from "../../../types/Operation"; +import { JsValueWrap } from "../../AccountDrawer/JsValueWrap"; +import { SignButton } from "../SignButton"; +import { SignPageFee } from "../SignPageFee"; +import { headerText } from "../SignPageHeader"; + +export const OriginationOperationSignModal: React.FC = ({ + operation, + fee, + message, +}) => { + const { isSigning, onSign, network } = useSignWithBeacon(operation, message); + const { code, storage } = operation.operations[0] as ContractOrigination; + + return ( + + + + Operation Request + + + {message.appMetadata.name} is requesting permission to sign this operation. + + + + + Network: + + + {capitalize(message.network.type)} + + + + + + + + + + {message.appMetadata.name} + + + + + + + + + + + Code + + + + + + + + + + + + + Storage + + + + + + + + + + + + + + ); +}; diff --git a/src/utils/beacon/useHandleBeaconMessage.test.tsx b/src/utils/beacon/useHandleBeaconMessage.test.tsx index bf5667086a..00786c5564 100644 --- a/src/utils/beacon/useHandleBeaconMessage.test.tsx +++ b/src/utils/beacon/useHandleBeaconMessage.test.tsx @@ -387,7 +387,8 @@ describe("partialOperationToOperation", () => { without( Object.values(TezosOperationType), TezosOperationType.TRANSACTION, - TezosOperationType.DELEGATION + TezosOperationType.DELEGATION, + TezosOperationType.ORIGINATION ) )("for %s", kind => { it("throws an error", () => { diff --git a/src/utils/beacon/useHandleBeaconMessage.tsx b/src/utils/beacon/useHandleBeaconMessage.tsx index adb88cdbc1..d297b25839 100644 --- a/src/utils/beacon/useHandleBeaconMessage.tsx +++ b/src/utils/beacon/useHandleBeaconMessage.tsx @@ -18,7 +18,7 @@ import { ImplicitAccount } from "../../types/Account"; import { ImplicitOperations } from "../../types/AccountOperations"; import { parseImplicitPkh, parsePkh } from "../../types/Address"; import { Network } from "../../types/Network"; -import { Operation } from "../../types/Operation"; +import { ContractOrigination, Operation } from "../../types/Operation"; import { useGetOwnedAccountSafe } from "../hooks/getAccountDataHooks"; import { useFindNetwork } from "../hooks/networkHooks"; import { useAsyncActionHandler } from "../hooks/useAsyncActionHandler"; @@ -194,6 +194,20 @@ export const partialOperationToOperation = ( return { type: "undelegation", sender: signer.address }; } } + case TezosOperationType.ORIGINATION: { + const { script } = partialOperation; + const { code, storage } = script as unknown as { + code: ContractOrigination["code"]; + storage: ContractOrigination["storage"]; + }; + + return { + type: "contract_origination", + sender: signer.address, + code, + storage, + }; + } default: throw new Error(`Unsupported operation kind: ${partialOperation.kind}`); }