Skip to content

Commit

Permalink
Add batch support for Multisig accounts
Browse files Browse the repository at this point in the history
  • Loading branch information
serjonya-trili committed Jul 27, 2023
1 parent 3dcb8c0 commit 3dea66b
Show file tree
Hide file tree
Showing 21 changed files with 339 additions and 293 deletions.
69 changes: 30 additions & 39 deletions src/components/CSVFileUploader/CSVFileUploadForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,31 @@ import {
import Papa, { ParseResult } from "papaparse";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { ImplicitAccount } from "../../types/Account";
import { Account } from "../../types/Account";
import { Operation } from "../../types/Operation";
import { useGetImplicitAccount } from "../../utils/hooks/accountHooks";
import { useClearBatch, useSelectedNetwork } from "../../utils/hooks/assetsHooks";
import { RawPkh } from "../../types/Address";
import { useGetBestSignerForAccount, useGetOwnedAccount } from "../../utils/hooks/accountHooks";
import { useSelectedNetwork } from "../../utils/hooks/assetsHooks";
import { useGetToken } from "../../utils/hooks/tokensHooks";
import { useAppDispatch } from "../../utils/redux/hooks";
import { estimateAndUpdateBatch } from "../../utils/redux/thunks/estimateAndUpdateBatch";
import { OwnedImplicitAccountsAutocomplete } from "../AddressAutocomplete";
import { OwnedAccountsAutocomplete } from "../AddressAutocomplete";
import { parseOperation } from "./utils";
import { makeFormOperations } from "../sendForm/types";

type FormFields = {
sender: string;
sender: RawPkh;
file: FileList;
};

// TODO: add support for multisig
const CSVFileUploadForm = ({ onClose }: { onClose: () => void }) => {
const network = useSelectedNetwork();
const toast = useToast();
const getToken = useGetToken();
const dispatch = useAppDispatch();
const clearBatch = useClearBatch();
const getAccount = useGetImplicitAccount();
const getAccount = useGetOwnedAccount();
const getSigner = useGetBestSignerForAccount();

const [isLoading, setIsLoading] = useState(false);

const form = useForm<FormFields>({
Expand All @@ -49,38 +51,32 @@ const CSVFileUploadForm = ({ onClose }: { onClose: () => void }) => {
formState: { isValid, errors },
} = form;

const onCSVFileUploadComplete = async (sender: ImplicitAccount, rows: ParseResult<string[]>) => {
if (rows.errors.length > 0) {
throw new Error("Error loading csv file.");
}
const onCSVFileUploadComplete = async (sender: Account, rows: ParseResult<string[]>) => {
try {
if (rows.errors.length > 0) {
throw new Error("Error loading csv file: " + rows.errors.map(e => e.message).join(", "));
}

const operations: Operation[] = [];
for (let i = 0; i < rows.data.length; i++) {
const row = rows.data[i];
try {
operations.push(parseOperation(sender.address, row, getToken));
} catch (error: any) {
toast({
title: "error",
description: `Error at row #${i + 1}: ${error?.message}`,
status: "error",
});
return;
const operations: Operation[] = [];
for (let i = 0; i < rows.data.length; i++) {
const row = rows.data[i];
try {
operations.push(parseOperation(sender.address, row, getToken));
} catch (error: any) {
throw new Error(`Error at row #${i + 1}: ${error?.message}`);
}
}
}

try {
// TODO: add support for Multisig
await dispatch(estimateAndUpdateBatch(sender, sender, operations, network));
await dispatch(
estimateAndUpdateBatch(makeFormOperations(sender, getSigner(sender), operations), network)
);

toast({ title: "CSV added to batch!" });
toast({ title: "CSV added to batch!", status: "success" });
onClose();
} catch (error: any) {
clearBatch(sender.address.pkh);
toast({ title: "Invalid transaction", description: error.message, status: "error" });
} finally {
setIsLoading(false);
toast({ title: "Error", description: error.message, status: "error" });
}
setIsLoading(false);
};

const onSubmit = async ({ file, sender }: FormFields) => {
Expand All @@ -103,12 +99,7 @@ const CSVFileUploadForm = ({ onClose }: { onClose: () => void }) => {
<Text textAlign="center">Select an account and then upload the CSV file.</Text>
<ModalBody>
<FormControl paddingY={5} isInvalid={!!errors.sender}>
{/* TODO: Use AllAccountsAutocomplete instead */}
<OwnedImplicitAccountsAutocomplete
label="From"
inputName="sender"
allowUnknown={false}
/>
<OwnedAccountsAutocomplete label="From" inputName="sender" allowUnknown={false} />
{errors.sender && <FormErrorMessage>{errors.sender.message}</FormErrorMessage>}
</FormControl>

Expand Down
97 changes: 57 additions & 40 deletions src/components/sendForm/SendForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,17 +343,21 @@ describe("<SendForm />", () => {
});

expect(fakeTezosUtils.estimateBatch).toHaveBeenCalledWith(
[
{
type: "fa2",
amount: "1000000",
recipient: parsePkh("tz1Kt4P8BCaP93AEV4eA7gmpRryWt5hznjCP"),
sender: parsePkh("tz1ikfEcj3LmsmxpcC1RMZNzBHbEmybCc43D"),
contract: parseContractPkh(mockFA2.contract),
tokenId: mockFA2.tokenId,
},
],
mockImplicitAccount(2),
{
type: "implicit",
content: [
{
type: "fa2",
amount: "1000000",
recipient: parsePkh("tz1Kt4P8BCaP93AEV4eA7gmpRryWt5hznjCP"),
sender: parsePkh("tz1ikfEcj3LmsmxpcC1RMZNzBHbEmybCc43D"),
contract: parseContractPkh(mockFA2.contract),
tokenId: mockFA2.tokenId,
},
],
sender: mockImplicitAccount(2),
signer: mockImplicitAccount(2),
},
"mainnet"
);

Expand Down Expand Up @@ -452,17 +456,21 @@ describe("<SendForm />", () => {
});

expect(fakeTezosUtils.estimateBatch).toHaveBeenCalledWith(
[
{
type: "fa1.2",
amount: "1000000000",
recipient: parsePkh("tz1Kt4P8BCaP93AEV4eA7gmpRryWt5hznjCP"),
sender: parsePkh("tz1ikfEcj3LmsmxpcC1RMZNzBHbEmybCc43D"),
contract: parseContractPkh(mockFa1.contract),
tokenId: "0",
},
],
mockImplicitAccount(2),
{
type: "implicit",
content: [
{
type: "fa1.2",
amount: "1000000000",
recipient: parsePkh("tz1Kt4P8BCaP93AEV4eA7gmpRryWt5hznjCP"),
sender: parsePkh("tz1ikfEcj3LmsmxpcC1RMZNzBHbEmybCc43D"),
contract: parseContractPkh(mockFa1.contract),
tokenId: "0",
},
],
sender: mockImplicitAccount(2),
signer: mockImplicitAccount(2),
},
"mainnet"
);

Expand Down Expand Up @@ -729,6 +737,10 @@ describe("<SendForm />", () => {
});

describe("Multisig", () => {
beforeEach(() => {
store.dispatch(multisigActions.setMultisigs(multisigs));
});

it("hides the signer input if only one available", async () => {
render(fixture(multisigs[0].address.pkh, { type: "tez" }));
expect(screen.queryByTestId("real-address-input-proposalSigner")).not.toBeInTheDocument();
Expand All @@ -743,9 +755,11 @@ describe("<SendForm />", () => {
});

test("User can acomplish a tez proposal", async () => {
fakeTezosUtils.estimateMultisigPropose.mockResolvedValueOnce({
suggestedFeeMutez: 12345,
} as Estimate);
fakeTezosUtils.estimateBatch.mockResolvedValueOnce([
{
suggestedFeeMutez: 12345,
} as Estimate,
]);
fakeTezosUtils.proposeMultisigLambda.mockResolvedValueOnce({
hash: "mockHash",
} as TransactionOperation);
Expand All @@ -772,10 +786,10 @@ describe("<SendForm />", () => {
expect(subTotal).toHaveTextContent(/23 ꜩ/i);

const fee = screen.getByLabelText(/^fee$/i);
expect(fee).toHaveTextContent(/0.012345/i);
expect(fee).toHaveTextContent(/0 ꜩ/i);

const total = screen.getByLabelText(/^total$/i);
expect(total).toHaveTextContent(/23.012345/i);
expect(total).toHaveTextContent(/23 ꜩ/i);
});

fillPassword("mockPass");
Expand All @@ -799,9 +813,11 @@ describe("<SendForm />", () => {
});

test("User can acomplish an FA2 proposal", async () => {
fakeTezosUtils.estimateMultisigPropose.mockResolvedValueOnce({
suggestedFeeMutez: 12345,
} as Estimate);
fakeTezosUtils.estimateBatch.mockResolvedValueOnce([
{
suggestedFeeMutez: 12345,
} as Estimate,
]);
fakeTezosUtils.proposeMultisigLambda.mockResolvedValueOnce({
hash: "mockHash",
} as TransactionOperation);
Expand All @@ -821,10 +837,10 @@ describe("<SendForm />", () => {

await waitFor(() => {
const fee = screen.getByLabelText(/^fee$/i);
expect(fee).toHaveTextContent(/0.012345/i);
expect(fee).toHaveTextContent(/0 ꜩ/i);

const total = screen.getByLabelText(/^total$/i);
expect(total).toHaveTextContent(/0.012345/i);
expect(total).toHaveTextContent(/0 ꜩ/i);
});

fillPassword("mockPass");
Expand Down Expand Up @@ -860,21 +876,22 @@ describe("<SendForm />", () => {
decimals: "8",
},
};
fakeTezosUtils.estimateMultisigPropose.mockResolvedValueOnce({
suggestedFeeMutez: 12345,
} as Estimate);
fakeTezosUtils.estimateBatch.mockResolvedValueOnce([
{
suggestedFeeMutez: 12345,
} as Estimate,
]);
fakeTezosUtils.proposeMultisigLambda.mockResolvedValueOnce({
hash: "mockHash",
} as TransactionOperation);

render(
fixture(MOCK_PKH, {
fixture(multisigs[0].address.pkh, {
type: "token",
data: mockFa1,
})
);

selectSender("Multisig Account 1");

const recipientInput = screen.getByLabelText(/to/i);
fireEvent.change(recipientInput, { target: { value: mockImplicitAddress(7).pkh } });

Expand All @@ -890,10 +907,10 @@ describe("<SendForm />", () => {

await waitFor(() => {
const fee = screen.getByLabelText(/^fee$/i);
expect(fee).toHaveTextContent(/0.012345/i);
expect(fee).toHaveTextContent(/0 ꜩ/i);

const total = screen.getByLabelText(/^total$/i);
expect(total).toHaveTextContent(/0.012345/i);
expect(total).toHaveTextContent(/0 ꜩ/i);
});

fillPassword("mockPass");
Expand Down
24 changes: 12 additions & 12 deletions src/components/sendForm/SendForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { TezosToolkit, TransferParams } from "@taquito/taquito";
import { useEffect, useRef, useState } from "react";
import { RawPkh } from "../../types/Address";
import { Operation } from "../../types/Operation";
import { useGetImplicitAccount } from "../../utils/hooks/accountHooks";
import { useGetBestSignerForAccount, useGetOwnedAccount } 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 { getTotalFee, operationsToBatchItems } from "../../views/batch/batchUtils";
import { FillStep } from "./steps/FillStep";
import { SubmitStep } from "./steps/SubmitStep";
import { SuccessStep } from "./steps/SuccessStep";
import { EstimatedOperation, FormOperations, SendFormMode } from "./types";
import { EstimatedOperation, FormOperations, makeFormOperations, SendFormMode } from "./types";
import { makeTransfer } from "./util/execution";
import { makeSimulation } from "./util/simulation";

export const SendForm = ({
sender,
Expand All @@ -33,7 +33,8 @@ export const SendForm = ({
const toast = useToast();
const dispatch = useAppDispatch();
const clearBatch = useClearBatch();
const getAccount = useGetImplicitAccount();
const getAccount = useGetOwnedAccount();
const getSigner = useGetBestSignerForAccount();

const [isLoading, setIsLoading] = useState(false);

Expand All @@ -56,11 +57,11 @@ export const SendForm = ({
setIsLoading(true);

try {
const estimate = await makeSimulation(operations, network);
const estimates = await operationsToBatchItems(operations, network);

setTransferValues({
operations,
fee: String(estimate),
fee: String(getTotalFee(estimates)),
});
} catch (error: any) {
console.warn("Simulation Error", error);
Expand All @@ -71,16 +72,17 @@ export const SendForm = ({
};

const addToBatch = async (operation: Operation, senderPkh: RawPkh) => {
// TODO: add support for Multisig
if (isLoading) {
return;
}
setIsLoading(true);
const sender = getAccount(senderPkh);
const signer = getSigner(sender);

try {
// TODO: add support for Multisig
await dispatch(estimateAndUpdateBatch(sender, sender, [operation], network));
await dispatch(
estimateAndUpdateBatch(makeFormOperations(sender, signer, [operation]), network)
);

toast({ title: "Transaction added to batch!", status: "success" });
} catch (error: any) {
Expand All @@ -91,7 +93,6 @@ export const SendForm = ({
};

const execute = async (operations: FormOperations, tezosToolkit: TezosToolkit) => {
// TODO: add support for Multisig
if (isLoading) {
return;
}
Expand All @@ -101,8 +102,7 @@ export const SendForm = ({
const result = await makeTransfer(operations, tezosToolkit);
if (mode.type === "batch") {
// TODO this will have to me moved in a thunk
const batchOwner = operations.signer.address.pkh;
clearBatch(batchOwner);
clearBatch(operations.sender);
}
setHash(result.hash);
toast({ title: "Success", description: result.hash });
Expand Down
Loading

0 comments on commit 3dea66b

Please sign in to comment.