Skip to content

Commit

Permalink
Add origination operation modal
Browse files Browse the repository at this point in the history
  • Loading branch information
OKendigelyan committed May 24, 2024
1 parent 774354b commit 567431d
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 3 deletions.
4 changes: 3 additions & 1 deletion src/components/SendFlow/Beacon/BeaconSignPage.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -20,6 +21,8 @@ export const BeaconSignPage: React.FC<BeaconSignPageProps> = ({ operation, fee,
case "undelegation": {
return <UndelegationSignPage fee={fee} message={message} operation={operation} />;
}
case "contract_origination":
return <OriginationOperationSignModal fee={fee} message={message} operation={operation} />;
/**
* FA1/2 are impossible to get here because we don't parse them
* instead we get a generic contract call
Expand All @@ -28,7 +31,6 @@ export const BeaconSignPage: React.FC<BeaconSignPageProps> = ({ operation, fee,
*/
case "fa1.2":
case "fa2":
case "contract_origination":
throw new Error("Unsupported operation type");
}
};
Original file line number Diff line number Diff line change
@@ -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";

Check warning on line 20 in src/components/SendFlow/Beacon/OriginationOperationSignModal.test.tsx

View workflow job for this annotation

GitHub Actions / test

There should be at least one empty line between import groups

Check warning on line 20 in src/components/SendFlow/Beacon/OriginationOperationSignModal.test.tsx

View workflow job for this annotation

GitHub Actions / test

`../../../types/Network` import should occur before import of `../../../utils/beacon/WalletClient`
import { TezosToolkit } from "@taquito/taquito";

Check warning on line 21 in src/components/SendFlow/Beacon/OriginationOperationSignModal.test.tsx

View workflow job for this annotation

GitHub Actions / test

`@taquito/taquito` import should occur before type import of `@taquito/taquito/dist/types/wallet/batch-operation`

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("<OriginationOperationSignModal />", () => {
it("renders fee", () => {
render(<OriginationOperationSignModal fee={fee} message={message} operation={operation} />);

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(<OriginationOperationSignModal fee={fee} message={message} operation={operation} />);

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(<SuccessStep hash="ophash" />);
});
});
113 changes: 113 additions & 0 deletions src/components/SendFlow/Beacon/OriginationOperationSignModal.tsx
Original file line number Diff line number Diff line change
@@ -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<BeaconSignPageProps> = ({
operation,
fee,
message,
}) => {
const { isSigning, onSign, network } = useSignWithBeacon(operation, message);
const { code, storage } = operation.operations[0] as ContractOrigination;

return (
<ModalContent>
<ModalHeader marginBottom="24px">
<Flex alignItems="center" justifyContent="center">
Operation Request
</Flex>
<Text marginTop="10px" color={colors.gray[400]} textAlign="center" size="sm">
{message.appMetadata.name} is requesting permission to sign this operation.
</Text>

<Flex alignItems="center" justifyContent="center" marginTop="10px">
<Heading marginRight="4px" color={colors.gray[450]} size="sm">
Network:
</Heading>
<Text color={colors.gray[400]} size="sm">
{capitalize(message.network.type)}
</Text>
</Flex>
</ModalHeader>
<ModalCloseButton />
<ModalBody data-testid="beacon-request-body">
<Flex
alignItems="center"
marginTop="16px"
padding="15px"
borderRadius="4px"
backgroundColor={colors.gray[800]}
>
<AspectRatio width="60px" marginRight="12px" ratio={1}>
<Image borderRadius="4px" src={message.appMetadata.icon} />
</AspectRatio>
<Heading size="sm">{message.appMetadata.name}</Heading>
</Flex>

<Flex alignItems="center" justifyContent="end" marginTop="12px">
<SignPageFee fee={fee} />
</Flex>

<Accordion marginTop="16px" allowToggle={true}>
<AccordionItem background={colors.gray[800]} border="none" borderRadius="8px">
<AccordionButton>
<Heading flex="1" textAlign="left" marginY="10px" size="md">
Code
</Heading>
<AccordionIcon />
</AccordionButton>
<AccordionPanel overflowY="auto" maxHeight="300px">
<JsValueWrap value={code} />
</AccordionPanel>
</AccordionItem>
</Accordion>
<Accordion marginTop="16px" allowToggle={true}>
<AccordionItem background={colors.gray[800]} border="none" borderRadius="8px">
<AccordionButton>
<Heading flex="1" textAlign="left" marginY="10px" size="md">
Storage
</Heading>
<AccordionIcon />
</AccordionButton>
<AccordionPanel overflowY="auto" maxHeight="300px">
<JsValueWrap value={storage} />
</AccordionPanel>
</AccordionItem>
</Accordion>
</ModalBody>
<ModalFooter padding="16px 0 0 0">
<SignButton
isLoading={isSigning}
network={network}
onSubmit={onSign}
signer={operation.signer}
text={headerText(operation.type, "single")}
/>
</ModalFooter>
</ModalContent>
);
};
3 changes: 2 additions & 1 deletion src/utils/beacon/useHandleBeaconMessage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
16 changes: 15 additions & 1 deletion src/utils/beacon/useHandleBeaconMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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}`);
}
Expand Down

0 comments on commit 567431d

Please sign in to comment.