diff --git a/src/ExtrinsicSubmissionStateView.tsx b/src/ExtrinsicSubmissionStateView.tsx
index b87747fe..a8860244 100644
--- a/src/ExtrinsicSubmissionStateView.tsx
+++ b/src/ExtrinsicSubmissionStateView.tsx
@@ -4,6 +4,7 @@ import ExtrinsicSubmissionResult from './ExtrinsicSubmissionResult';
export interface Props {
successMessage?: string;
slim?: boolean;
+ submissionId?: string;
}
export default function ExtrinsicSubmissionStateView(props: Props) {
@@ -11,8 +12,8 @@ export default function ExtrinsicSubmissionStateView(props: Props) {
if(extrinsicSubmissionState.submitted) {
return ();
diff --git a/src/common/WalletGauge.tsx b/src/common/WalletGauge.tsx
index 0028f9ec..bc3755b7 100644
--- a/src/common/WalletGauge.tsx
+++ b/src/common/WalletGauge.tsx
@@ -59,8 +59,10 @@ export default function WalletGauge(props: Props) {
};
}, [ amount, destination, unit, mutateBalanceState, signer, expectNewTransaction ]);
- const transferCallback = useCallback(() => {
- submitCall(transfer);
+ const transferCallback = useCallback(async () => {
+ try {
+ await submitCall(transfer);
+ } catch(e) {}
}, [ transfer, submitCall ]);
const clearFormCallback = useCallback(() => {
@@ -131,7 +133,7 @@ export default function WalletGauge(props: Props) {
buttonText: "Cancel",
buttonVariant: 'secondary-polkadot',
callback: cancelCallback,
- disabled: extrinsicSubmissionState.submitted && !extrinsicSubmissionState.callEnded,
+ disabled: extrinsicSubmissionState.inProgress,
},
{
id: 'transfer',
diff --git a/src/loc/CloseLocButton.tsx b/src/loc/CloseLocButton.tsx
index c13bcb7b..c7b3b1dc 100644
--- a/src/loc/CloseLocButton.tsx
+++ b/src/loc/CloseLocButton.tsx
@@ -25,8 +25,7 @@ enum CloseStatus {
NONE,
ACCEPT,
CLOSE_PENDING,
- CLOSING,
- DONE
+ CLOSING
}
interface CloseState {
@@ -60,9 +59,11 @@ export default function CloseLocButton(props: Props) {
});
}, [ mutateLocState, autoAck, signer ]);
- const close = useCallback(() => {
+ const close = useCallback(async () => {
setCloseState({ status: CloseStatus.CLOSING });
- submitCall(closeCall);
+ try {
+ await submitCall(closeCall);
+ } catch(e) {}
}, [ closeCall, submitCall ]);
const clear = useCallback(() => {
diff --git a/src/loc/ImportItems.test.tsx b/src/loc/ImportItems.test.tsx
index 2a068374..73624654 100644
--- a/src/loc/ImportItems.test.tsx
+++ b/src/loc/ImportItems.test.tsx
@@ -6,7 +6,6 @@ import { setLocState, refresh } from "./__mocks__/UserLocContextMock";
import { CollectionItem, Fees, UUID } from "@logion/node-api";
import { H256 } from "@logion/node-api/dist/types/interfaces";
import { LogionClient, AddCollectionItemParams, EstimateFeesAddCollectionItemParams } from "@logion/client";
-import { mockSubmittableResult } from "../logion-chain/__mocks__/SignatureMock";
import { SUCCESSFUL_SUBMISSION, setClientMock, setExtrinsicSubmissionState } from 'src/logion-chain/__mocks__/LogionChainMock';
import { It, Mock } from 'moq.ts';
import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
@@ -14,7 +13,6 @@ import { Compact, u128 } from "@polkadot/types-codec";
import { TEST_WALLET_USER } from 'src/wallet-user/TestData';
jest.mock("../common/CommonContext");
-jest.mock("../logion-chain/Signature");
jest.mock("../logion-chain");
jest.mock("./UserLocContext");
@@ -29,9 +27,7 @@ describe("ImportItems", () => {
await userEvent.click(importAllButton);
await waitFor(() => expect(screen.getByRole("button", { name: "Proceed" })).toBeVisible());
await clickByName("Proceed");
- await waitFor(() => expect(screen.getByRole("button", { name: "Proceed" })).toBeVisible());
- await clickByName("Proceed");
- await waitFor(() => expect(collection.addCollectionItem).toBeCalled());
+ await waitFor(() => expect(collection.addCollectionItem).toBeCalledTimes(2));
await waitFor(() => expect(screen.getAllByRole("img", {name: "ok"}).length).toBe(2));
@@ -69,7 +65,6 @@ async function uploadCsv(): Promise {
const collection = {
addCollectionItem: jest.fn((params: AddCollectionItemParams) => {
- params.callback!(mockSubmittableResult(true, "finalized"));
return Promise.resolve();
}),
estimateFeesAddCollectionItem: jest.fn((params: EstimateFeesAddCollectionItemParams) => {
diff --git a/src/loc/ImportItems.tsx b/src/loc/ImportItems.tsx
index d5944307..b37921f8 100644
--- a/src/loc/ImportItems.tsx
+++ b/src/loc/ImportItems.tsx
@@ -10,7 +10,7 @@ import {
CreativeCommons,
} from "@logion/client";
import { Fees, Hash } from '@logion/node-api';
-import { useCallback, useState } from "react";
+import { useCallback, useMemo, useState } from "react";
import { OverlayTrigger, Spinner, Tooltip } from "react-bootstrap";
import Dialog from "../common/Dialog";
@@ -25,28 +25,24 @@ import { useUserLocContext } from "./UserLocContext";
import ImportItemDetails, { ErrorType, Item } from "./ImportItemDetails";
import './ImportItems.css';
-import ClientExtrinsicSubmitter, { Call, CallCallback } from "../ClientExtrinsicSubmitter";
import { CsvItem, readItemsCsv } from "./ImportCsvReader";
import Alert from "src/common/Alert";
import { UUID } from "@logion/node-api";
import config from "../config";
import EstimatedFees from "./fees/EstimatedFees";
import { BrowserFile } from "@logion/client-browser";
-
-type Submitters = Record;
+import { Call, CallBatch, CallCallback } from "src/logion-chain/LogionChainContext";
+import ExtrinsicSubmissionStateView from "src/ExtrinsicSubmissionStateView";
export default function ImportItems() {
const { width } = useResponsiveContext();
- const { signer } = useLogionChain();
+ const { signer, submitCallBatch, extrinsicSubmissionState, clearSubmissionState } = useLogionChain();
const { colorTheme } = useCommonContext();
const { refresh, locState } = useUserLocContext();
const [ showImportItems, setShowImportItems ] = useState(false);
const [ items, setItems ] = useState- ([]);
const [ csvReadError, setCsvReadError ] = useState("");
- const [ submitters, setSubmitters ] = useState({});
- const [ currentItem, setCurrentItem ] = useState(0);
- const [ isBatchImport, setIsBatchImport ] = useState(false);
const [ uploading, setUploading ] = useState(false);
const [ fees, setFees ] = useState(null);
const [ itemToSubmit, setItemToSubmit ] = useState
- ();
@@ -54,7 +50,7 @@ export default function ImportItems() {
const readCsvFile = useCallback(async (file: File) => {
const collection = locState as ClosedCollectionLoc;
const acceptsUpload = collectionAcceptsUpload(collection);
- setSubmitters({});
+ clearSubmissionState();
const result = await readItemsCsv(file);
if("items" in result) {
@@ -74,95 +70,132 @@ export default function ImportItems() {
setCsvReadError(result.error);
}
setShowImportItems(true);
- }, [ setSubmitters, locState ]);
+ }, [ clearSubmissionState, locState ]);
+
+ const itemFees = useCallback(async (item: Item) => {
+ const collection = locState as ClosedCollectionLoc;
+ return await collection.estimateFeesAddCollectionItem({
+ itemId: item.id!,
+ itemDescription: item.description,
+ itemFiles: item.files,
+ restrictedDelivery: item.restrictedDelivery,
+ itemToken: item.token,
+ logionClassification: item.logionClassification,
+ specificLicenses: item.specificLicense ? [ item.specificLicense ] : undefined,
+ creativeCommons: item.creativeCommons,
+ });
+ }, [ locState ]);
const submitItem = useCallback(async (item: Item) => {
if (locState) {
setItemToSubmit(item);
- const collection = locState as ClosedCollectionLoc;
- const fees = await collection.estimateFeesAddCollectionItem({
- itemId: item.id!,
- itemDescription: item.description,
- itemFiles: item.files,
- restrictedDelivery: item.restrictedDelivery,
- itemToken: item.token,
- logionClassification: item.logionClassification,
- specificLicenses: item.specificLicense ? [ item.specificLicense ] : undefined,
- creativeCommons: item.creativeCommons,
- })
+ const fees = await itemFees(item);
setFees(fees);
}
- }, [ locState ]);
-
- const doSubmitItem = useCallback(async () => {
- if(itemToSubmit) {
- setFees(null);
-
- const collection = locState as ClosedCollectionLoc;
- const item = itemToSubmit;
- setItemToSubmit(undefined);
-
- item.submitted = true;
- setItems(items.slice());
- const signAndSubmit: Call = async (callback: CallCallback) => {
- await collection.addCollectionItem({
- signer: signer!,
- itemId: item.id!,
- itemDescription: item.description,
- itemFiles: item.files,
- restrictedDelivery: item.restrictedDelivery,
- itemToken: item.token,
- logionClassification: item.logionClassification,
- specificLicenses: item.specificLicense ? [ item.specificLicense ] : undefined,
- creativeCommons: item.creativeCommons,
- callback,
- })
- }
- const newSubmitters = { ...submitters };
- newSubmitters[item.id!.toHex()] = signAndSubmit;
- setSubmitters(newSubmitters);
- }
- }, [ submitters, signer, locState, itemToSubmit, items ]);
-
- const submitNext = useCallback(async (submittedItem: Item, failed: boolean) => {
- if(failed) {
- submittedItem.failed = true;
- } else {
- submittedItem.success = true;
- }
- setItems(items.slice());
- if(isBatchImport) {
- const nextItemIndex = getNextItem(items, currentItem);
- if(nextItemIndex < items.length) {
- setCurrentItem(nextItemIndex);
- await submitItem(items[nextItemIndex]);
- } else {
- setIsBatchImport(false);
- }
- }
- }, [ isBatchImport, currentItem, items, setCurrentItem, submitItem, setIsBatchImport ]);
+ }, [ locState, itemFees ]);
const cancelSubmission = useCallback(() => {
- setIsBatchImport(false);
setFees(null);
setItemToSubmit(undefined);
- }, []);
+ clearSubmissionState();
+ }, [ clearSubmissionState ]);
const close = useCallback(() => {
setShowImportItems(false);
- setIsBatchImport(false);
setItems([]);
refresh();
- }, [ setShowImportItems, setIsBatchImport, refresh ]);
+ clearSubmissionState();
+ }, [ setShowImportItems, refresh, clearSubmissionState ]);
+
+ const callMap = useMemo(() => {
+ const collection = locState as ClosedCollectionLoc;
+ const map: Record = {};
+ items.forEach(item => {
+ if(!item.submitted && item.id) {
+ map[item.id.toHex()] = async (callback: CallCallback) => {
+ await collection.addCollectionItem({
+ signer: signer!,
+ itemId: item.id!,
+ itemDescription: item.description,
+ itemFiles: item.files,
+ restrictedDelivery: item.restrictedDelivery,
+ itemToken: item.token,
+ logionClassification: item.logionClassification,
+ specificLicenses: item.specificLicense ? [ item.specificLicense ] : undefined,
+ creativeCommons: item.creativeCommons,
+ callback,
+ })
+ };
+ }
+ });
+ return map;
+ }, [ items, locState, signer ]);
+
+ const callBatch = useMemo(() => {
+ return new CallBatch(Object.keys(callMap).map(submissionId => ({
+ submissionId,
+ call: callMap[submissionId],
+ })));
+ }, [ callMap ]);
+
+ const doSubmitItem = useCallback(async () => {
+ setFees(null);
+ if(itemToSubmit && itemToSubmit.id) {
+ const submissionId = itemToSubmit.id.toHex();
+ const errors = await submitCallBatch(CallBatch.fromSingleWithId(submissionId, callMap[submissionId]));
+ setItems(items.map(item => {
+ if(item === itemToSubmit) {
+ const error = errors[submissionId];
+ return {
+ ...item,
+ submitted: true,
+ errorType: error ? "chain" : undefined,
+ error: error ? String(error) : undefined,
+ success: !error,
+ };
+ } else {
+ return item;
+ }
+ }));
+ } else {
+ const errors = await submitCallBatch(callBatch);
+ setItems(items.map(item => {
+ if(item.id && item.id.toHex() in callMap) {
+ const submissionId = item.id.toHex();
+ const error = errors[submissionId];
+ return {
+ ...item,
+ submitted: true,
+ errorType: submissionId in errors ? "chain" : undefined,
+ error: error ? String(error) : undefined,
+ success: !error,
+ };
+ } else {
+ return item;
+ }
+ }));
+ }
+ clearSubmissionState();
+ }, [ itemToSubmit, callBatch, callMap, items, submitCallBatch, clearSubmissionState ]);
+
+ const batchFees = useCallback(async () => {
+ let total = new Fees({ inclusionFee: 0n });
+ for(const itemId of Object.keys(callMap)) {
+ const item = items.find(item => item.id?.toHex() === itemId);
+ if(item) {
+ const fees = await itemFees(item);
+ total = addFees(total, fees);
+ }
+ }
+ return total;
+ }, [ callMap, itemFees, items ]);
const importAll = useCallback(async () => {
if(items.length > 0) {
- setIsBatchImport(true);
- const firstItemIndex = getFirstItem(items);
- setCurrentItem(firstItemIndex);
- await submitItem(items[firstItemIndex]);
+ setItemToSubmit(undefined);
+ setFees(await batchFees());
}
- }, [ setIsBatchImport, setCurrentItem, submitItem, items ]);
+ }, [ setItemToSubmit, items, batchFees ]);
const uploadItemFile = useCallback(async (item: Item, file: File) => {
setUploading(true);
@@ -207,7 +240,7 @@ export default function ImportItems() {
buttonText: "Close",
callback: close,
buttonVariant: "primary",
- disabled: isBatchImport
+ disabled: extrinsicSubmissionState.inProgress,
}
]}
show={ showImportItems }
@@ -233,7 +266,7 @@ export default function ImportItems() {
@@ -262,7 +295,7 @@ export default function ImportItems() {
render: item => (
<>
{
- (!item.submitted && !item.error) &&
+ item.id && (!item.submitted && !item.error) && !extrinsicSubmissionState.inProgress &&
}
{
- (item.submitted && !item.success && submitters[item.id?.toHex() || ""] !== undefined && submitters[item.id?.toHex() || ""] !== null) &&
+ item.id && extrinsicSubmissionState.isInProgress(item.id.toHex()) &&
submitNext(item, true) }
- onSuccess={ () => submitNext(item, false) }
+
} />
@@ -338,7 +369,7 @@ export default function ImportItems() {
@@ -382,19 +413,6 @@ export default function ImportItems() {
);
}
-function getFirstItem(items: Item[]): number {
- return getNextItem(items, -1);
-}
-
-
-function getNextItem(items: Item[], current: number): number {
- let next = current + 1;
- while(next < items.length && (items[next].error || items[next].submitted)) {
- ++next;
- }
- return next;
-}
-
function getNotSubmitted(items: Item[]): number {
let count = 0;
for(const item of items) {
@@ -542,3 +560,34 @@ function validateTermsAndConditions(csvItem: CsvItem): TCValidationResult {
return { tcError: "" + error }
}
}
+
+function addFees(total: Fees, term: Fees): Fees {
+ const inclusionFee = total.inclusionFee + term.inclusionFee;
+ const storageFee = addFeesTerms(total.storageFee, term.storageFee);
+ const legalFee = addFeesTerms(total.legalFee, term.legalFee);
+ const certificateFee = addFeesTerms(total.certificateFee, term.certificateFee);
+ const valueFee = addFeesTerms(total.valueFee, term.valueFee);
+ const collectionItemFee = addFeesTerms(total.collectionItemFee, term.collectionItemFee);
+ const tokensRecordFee = addFeesTerms(total.tokensRecordFee, term.tokensRecordFee);
+ return new Fees({
+ inclusionFee,
+ storageFee,
+ legalFee,
+ certificateFee,
+ valueFee,
+ collectionItemFee,
+ tokensRecordFee,
+ });
+}
+
+function addFeesTerms(term1?: bigint, term2?: bigint): bigint | undefined {
+ if(term1 !== undefined && term2 !== undefined) {
+ return term1 + term2;
+ } else if(term1 !== undefined && term2 === undefined) {
+ return term1;
+ } else if(term1 === undefined && term2 !== undefined) {
+ return term2;
+ } else {
+ return undefined;
+ }
+}
diff --git a/src/loc/LocItem.tsx b/src/loc/LocItem.tsx
index c19ea396..d379a5cf 100644
--- a/src/loc/LocItem.tsx
+++ b/src/loc/LocItem.tsx
@@ -243,10 +243,8 @@ export type LocItem = LinkItem | MetadataItem | FileItem;
export enum PublishStatus {
NONE,
START,
- PUBLISH_PENDING,
PUBLISHING,
- PUBLISHED,
- ERROR
+ DONE
}
export interface PublishState {
diff --git a/src/loc/LocPublishButton.test.tsx b/src/loc/LocPublishButton.test.tsx
index 997237da..5aac5818 100644
--- a/src/loc/LocPublishButton.test.tsx
+++ b/src/loc/LocPublishButton.test.tsx
@@ -33,7 +33,7 @@ describe("LocPublishButton", () => {
}
);
const confirm = jest.fn();
- setExtrinsicSubmissionState(NO_SUBMISSION);
+ setExtrinsicSubmissionState(SUCCESSFUL_SUBMISSION);
render( {
await clickByName("Publish");
// Then item published
- await waitFor(() => expectSubmitting());
+ await waitFor(() => screen.getByText("LOC item successfully published"));
expect(confirm).toBeCalled();
});
diff --git a/src/loc/LocPublishButton.tsx b/src/loc/LocPublishButton.tsx
index 2f236c2f..2b18ed0a 100644
--- a/src/loc/LocPublishButton.tsx
+++ b/src/loc/LocPublishButton.tsx
@@ -1,32 +1,37 @@
import { Fees } from "@logion/node-api";
import Button from "../common/Button";
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo, useCallback } from "react";
import ProcessStep from "../common/ProcessStep";
-import Alert from "../common/Alert";
import { PublishProps, PublishState, PublishStatus, LinkItem, FileItem, MetadataItem } from "./LocItem";
import Icon from "../common/Icon";
import { useLocContext } from "./LocContext";
-import ClientExtrinsicSubmitter, { Call, CallCallback } from "src/ClientExtrinsicSubmitter";
import LocPublicDataDetails from "./LocPublicDataDetails";
import LocPrivateFileDetails from "./LocPrivateFileDetails";
import LocLinkDetails from "./LocLinkDetails";
import LocItemEstimatedFees from "./LocItemEstimatedFees";
+import { CallCallback, useLogionChain } from "src/logion-chain";
+import ExtrinsicSubmissionStateView from "src/ExtrinsicSubmissionStateView";
export default function LocPublishButton(props: PublishProps) {
const [ publishState, setPublishState ] = useState({ status: PublishStatus.NONE });
- const [ call, setCall ] = useState();
const { mutateLocState, loc } = useLocContext();
const [ fees, setFees ] = useState();
+ const { submitCall, clearSubmissionState } = useLogionChain();
- useEffect(() => {
- if (publishState.status === PublishStatus.PUBLISH_PENDING) {
- setPublishState({ status: PublishStatus.PUBLISHING });
- const call: Call = async (callback: CallCallback) =>
- mutateLocState(async current =>
- props.publishMutator(current, callback));
- setCall(() => call);
+ const call = useMemo(() => {
+ return async (callback: CallCallback) =>
+ mutateLocState(async current =>
+ props.publishMutator(current, callback));
+ }, [ mutateLocState, props ]);
+
+ const publish = useCallback(async () => {
+ setPublishState({ status: PublishStatus.PUBLISHING });
+ try {
+ await submitCall(call);
+ } finally {
+ setPublishState({ status: PublishStatus.DONE });
}
- }, [ props, publishState, setPublishState, mutateLocState ]);
+ }, [ submitCall, call ]);
useEffect(() => {
if(fees === undefined) {
@@ -37,6 +42,11 @@ export default function LocPublishButton(props: PublishProps) {
}
}, [ fees, props ]);
+ const cancel = useCallback(async () => {
+ clearSubmissionState();
+ setPublishState({ status: PublishStatus.NONE });
+ }, [ clearSubmissionState ]);
+
return (
<>
Publish
setPublishState({ status: PublishStatus.NONE })
+ callback: cancel,
},
{
buttonText: 'Publish',
buttonVariant: 'polkadot',
id: 'publish',
mayProceed: publishState.status === PublishStatus.START,
- callback: () => setPublishState({ status: PublishStatus.PUBLISH_PENDING })
+ callback: publish,
}
]}
>
@@ -87,44 +97,21 @@ export default function LocPublishButton(props: PublishProps) {
/>
- {
- setPublishState({ status: PublishStatus.PUBLISHED })
- } }
- onError={ () => setPublishState({ status: PublishStatus.ERROR }) }
- />
-
- setPublishState({ status: PublishStatus.NONE })
+ mayProceed: publishState.status === PublishStatus.DONE,
+ callback: cancel,
}
]}
>
- {
- publishState.status === PublishStatus.PUBLISHED &&
-
- LOC {props.itemType} successfully published
-
- }
- {
- publishState.status === PublishStatus.ERROR &&
-
- Failed to publish {props.itemType}
-
- }
+
>
)
diff --git a/src/loc/OpenLoc.tsx b/src/loc/OpenLoc.tsx
index e0736514..2fa8eff5 100644
--- a/src/loc/OpenLoc.tsx
+++ b/src/loc/OpenLoc.tsx
@@ -1,13 +1,12 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { Fees } from '@logion/node-api';
import { LocData } from '@logion/client';
-import { useLogionChain } from '../logion-chain';
+import { CallCallback, useLogionChain } from '../logion-chain';
import { useCommonContext } from '../common/CommonContext';
import ProcessStep from '../common/ProcessStep';
import CollectionLocMessage from '../loc/CollectionLocMessage';
import CollectionLimitsForm, { CollectionLimits, DEFAULT_LIMITS } from '../loc/CollectionLimitsForm';
import { useLocContext } from 'src/loc/LocContext';
-import { CallCallback } from '../ClientExtrinsicSubmitter';
import EstimatedFees, { getOtherFeesPaidBy, PAID_BY_REQUESTER } from './fees/EstimatedFees';
import { AcceptedRequest } from "@logion/client/dist/Loc";
import Button from 'src/common/Button';
diff --git a/src/loc/RequestVoteButton.test.tsx b/src/loc/RequestVoteButton.test.tsx
index 58a9f8e6..9ad3bce3 100644
--- a/src/loc/RequestVoteButton.test.tsx
+++ b/src/loc/RequestVoteButton.test.tsx
@@ -1,7 +1,6 @@
import { render, screen, waitFor } from "@testing-library/react";
import { clickByName } from "src/tests";
import RequestVoteButton from "./RequestVoteButton";
-import { mockSubmittableResult } from "src/logion-chain/__mocks__/SignatureMock";
import { ClosedLoc } from "src/__mocks__/@logion/client";
import { setLocState } from "./__mocks__/LocContextMock";
import { FAILED_SUBMISSION, NO_SUBMISSION, SUCCESSFUL_SUBMISSION, setExtrinsicSubmissionState } from "src/logion-chain/__mocks__/LogionChainMock";
@@ -16,7 +15,6 @@ describe("RequestVoteButton", () => {
const locState = new ClosedLoc();
setLocState(locState);
locState.legalOfficer.requestVote = async (params: any) => {
- params.callback(mockSubmittableResult(true));
return VOTE_ID;
};
setExtrinsicSubmissionState(NO_SUBMISSION);
@@ -30,7 +28,6 @@ describe("RequestVoteButton", () => {
const locState = new ClosedLoc();
setLocState(locState);
locState.legalOfficer.requestVote = async (params: any) => {
- params.callback(mockSubmittableResult(true));
return VOTE_ID;
};
setExtrinsicSubmissionState(SUCCESSFUL_SUBMISSION);
@@ -44,10 +41,7 @@ describe("RequestVoteButton", () => {
it("shows error on failure", async () => {
const locState = new ClosedLoc();
setLocState(locState);
- locState.legalOfficer.requestVote = async (params: any) => {
- params.callback(mockSubmittableResult(false, "ExtrinsicFailed", true));
- throw new Error();
- };
+ locState.legalOfficer.requestVote = async () => {};
setExtrinsicSubmissionState(FAILED_SUBMISSION);
render();
await clickByName((_, element) => /Request a vote/.test(element.textContent || ""));
diff --git a/src/logion-chain/LogionChainContext.tsx b/src/logion-chain/LogionChainContext.tsx
index 5608d431..2d0bef71 100644
--- a/src/logion-chain/LogionChainContext.tsx
+++ b/src/logion-chain/LogionChainContext.tsx
@@ -51,93 +51,183 @@ export type Call = (callback: CallCallback) => Promise;
export type SignAndSubmit = ((setResult: (result: ISubmittableResult | null) => void, setError: (error: unknown) => void) => void) | null;
+export type CallBatchJobs = { submissionId: string, call: Call }[];
+
+export class CallBatch {
+
+ static readonly DEFAULT_SUBMISSION_ID = "__DEFAULT__";
+
+ static fromSingle(call: Call) {
+ return CallBatch.fromSingleWithId(CallBatch.DEFAULT_SUBMISSION_ID, call);
+ }
+
+ static fromSingleWithId(submissionId: string, call: Call) {
+ return new CallBatch([ { submissionId, call } ]);
+ }
+
+ constructor(jobs: CallBatchJobs) {
+ this.ensureIdUnicity(jobs);
+ this.jobs = jobs;
+ }
+
+ private ensureIdUnicity(jobs: CallBatchJobs) {
+ const set = new Set();
+ jobs.forEach(job => set.add(job.submissionId));
+ if(set.size !== jobs.length) {
+ throw new Error("Unicity constraint on submission IDs is not met");
+ }
+ }
+
+ readonly jobs: CallBatchJobs;
+};
+
export class ExtrinsicSubmissionState {
constructor() {
- this._result = null;
- this._error = null;
- this._submitted = false;
- this._callEnded = false;
+ this._result = {};
+ this._error = {};
+ this._submitted = {};
+ this._callEnded = {};
+ this._submissions = 0;
}
- private _result: ISubmittableResult | null;
- private _error: any;
- private _submitted: boolean;
- private _callEnded: boolean;
+ private _result: Record;
+ private _error: Record;
+ private _submitted: Record;
+ private _callEnded: Record;
+ private _submissions: number;
canSubmit() {
return !this.submitted || this.callEnded;
}
get submitted() {
- return this._submitted;
+ return Object.keys(this._submitted).length > 0;
+ }
+
+ get callEnded() { // All submissions completed
+ return Object.keys(this._callEnded).length === this._submissions;
}
- get callEnded() {
- return this._callEnded;
+ get inProgress() {
+ return this.submitted && !this.callEnded;
}
- get result() {
- return this._result;
+ get result() { // Convenience property for single job submissions
+ return this.getResult();
}
- get error() {
- return this._error;
+ getResult(submissionId?: string) {
+ return this._result[submissionId || CallBatch.DEFAULT_SUBMISSION_ID] || null;
}
- submit() {
+ get error() { // Convenience property for single job submissions
+ return this.getError();
+ }
+
+ getError(submissionId?: string) {
+ return this._error[submissionId || CallBatch.DEFAULT_SUBMISSION_ID] || null;
+ }
+
+ isInProgress(submissionId?: string) {
+ return this.isSubmitted(submissionId) && !this.hasEnded(submissionId);
+ }
+
+ isSubmitted(submissionId?: string) {
+ return this._submitted[submissionId || CallBatch.DEFAULT_SUBMISSION_ID] || false;
+ }
+
+ hasEnded(submissionId?: string) {
+ return this._callEnded[submissionId || CallBatch.DEFAULT_SUBMISSION_ID] || false;
+ }
+
+ submit(submissions: number) {
+ if(!submissions) {
+ throw new Error("Cannot submit nothing");
+ }
if(!this.canSubmit()) {
throw new Error("Cannot submit new call");
}
const state = new ExtrinsicSubmissionState();
- state._result = null;
- state._error = null;
- state._submitted = true;
- state._callEnded = false;
+ state._result = {};
+ state._error = {};
+ state._submitted = {};
+ state._callEnded = {};
+ state._submissions = submissions;
+ return state;
+ }
+
+ start(submissionId: string) {
+ if(this._submitted[submissionId]) {
+ throw new Error("Call already started");
+ }
+ const state = new ExtrinsicSubmissionState();
+ state._submitted = {
+ ...this._submitted,
+ [ submissionId ]: true,
+ };
+ state._result = this._result;
+ state._error = this._error;
+ state._callEnded = this._callEnded;
+ state._submissions = this._submissions;
return state;
}
- withResult(result: ISubmittableResult) {
- if(this._callEnded) {
+ withResult(callId: string, result: ISubmittableResult) {
+ if(this._callEnded[callId]) {
throw new Error("Call already ended");
}
const state = new ExtrinsicSubmissionState();
- state._result = result;
+ state._result = {
+ ...this._result,
+ [ callId ]: result,
+ };
state._error = this._error;
state._submitted = this._submitted;
state._callEnded = this._callEnded;
+ state._submissions = this._submissions;
return state;
}
- end(error: unknown) {
- if(this._callEnded) {
+ end(submissionId: string, error: unknown) {
+ if(this._callEnded[submissionId]) {
throw new Error("Call already ended");
}
const state = new ExtrinsicSubmissionState();
state._result = this._result;
- state._error = error;
+ state._error = {
+ ...this._error,
+ [ submissionId ]: error,
+ }
state._submitted = this._submitted;
- state._callEnded = true;
+ state._callEnded = {
+ ...this._callEnded,
+ [ submissionId ]: true,
+ };
+ state._submissions = this._submissions;
return state;
}
- isSuccessful(): boolean {
- return this.callEnded && this.error === null;
+ isSuccessful(submissionId?: string): boolean {
+ const error = this.error[submissionId || CallBatch.DEFAULT_SUBMISSION_ID];
+ return this._callEnded[submissionId || CallBatch.DEFAULT_SUBMISSION_ID] && error === undefined;
}
- isError(): boolean {
- return this.callEnded && this.error !== null;
+ isError(submissionId?: string): boolean {
+ const error = this.error[submissionId || CallBatch.DEFAULT_SUBMISSION_ID];
+ return this._callEnded[submissionId || CallBatch.DEFAULT_SUBMISSION_ID] && error !== undefined;
}
resetAndKeepResult() {
- if(!this._callEnded) {
+ if(!this.callEnded) {
throw new Error("Call not yet ended");
}
const state = new ExtrinsicSubmissionState();
state._result = this._result;
state._error = this._error;
- state._submitted = false;
- state._callEnded = false;
+ state._submitted = {};
+ state._callEnded = {};
+ state._submissions = 0;
return state;
}
}
@@ -166,6 +256,7 @@ export interface LogionChainContextType {
tryEnableMetaMask: () => Promise,
extrinsicSubmissionState: ExtrinsicSubmissionState;
submitCall: (call: Call) => Promise;
+ submitCallBatch: (batch: CallBatch) => Promise>;
submitSignAndSubmit: (signAndSubmit: SignAndSubmit) => void;
resetSubmissionState: () => void;
clearSubmissionState: () => void;
@@ -200,6 +291,7 @@ const initState = (): FullLogionChainContextType => ({
tryEnableMetaMask: async () => {},
extrinsicSubmissionState: new ExtrinsicSubmissionState(),
submitCall: () => Promise.reject(),
+ submitCallBatch: () => Promise.reject(),
submitSignAndSubmit: () => {},
resetSubmissionState: () => {},
clearSubmissionState: () => {},
@@ -234,6 +326,8 @@ type ActionType = 'SET_SELECT_ADDRESS'
| 'RESET_SUBMISSION_STATE'
| 'SET_CLEAR_SUBMISSION_STATE'
| 'CLEAR_SUBMISSION_STATE'
+ | 'SET_SUBMIT_CALL_BATCH'
+ | 'SET_SUBMISSION_STARTED'
;
interface Action {
@@ -263,6 +357,9 @@ interface Action {
submitSignAndSubmit?: (signAndSubmit: SignAndSubmit) => void;
resetSubmissionState?: () => void;
clearSubmissionState?: () => void;
+ submissions?: number;
+ submissionId?: string;
+ submitCallBatch?: (batch: CallBatch) => Promise>;
}
function buildAxiosFactory(authenticatedClient?: LogionClient): AxiosFactory {
@@ -447,20 +544,25 @@ const reducer: Reducer = (state: FullLogionC
if(state.extrinsicSubmissionState.canSubmit()) {
return {
...state,
- extrinsicSubmissionState: state.extrinsicSubmissionState.submit(),
+ extrinsicSubmissionState: state.extrinsicSubmissionState.submit(action.submissions!),
};
} else {
return state;
}
+ case 'SET_SUBMISSION_STARTED':
+ return {
+ ...state,
+ extrinsicSubmissionState: state.extrinsicSubmissionState.start(action.submissionId!),
+ };
case 'SET_SUBMITTABLE_RESULT':
return {
...state,
- extrinsicSubmissionState: state.extrinsicSubmissionState.withResult(action.result!),
+ extrinsicSubmissionState: state.extrinsicSubmissionState.withResult(action.submissionId!, action.result!),
};
case 'SET_SUBMISSION_ENDED':
return {
...state,
- extrinsicSubmissionState: state.extrinsicSubmissionState.end(action.extrinsicSubmissionError || null),
+ extrinsicSubmissionState: state.extrinsicSubmissionState.end(action.submissionId!, action.extrinsicSubmissionError || null),
};
case 'SET_SUBMIT_SIGN_AND_SUBMIT':
return {
@@ -487,6 +589,11 @@ const reducer: Reducer = (state: FullLogionC
...state,
extrinsicSubmissionState: new ExtrinsicSubmissionState(),
};
+ case 'SET_SUBMIT_CALL_BATCH':
+ return {
+ ...state,
+ submitCallBatch: action.submitCallBatch!,
+ };
default:
/* istanbul ignore next */
throw new Error(`Unknown type: ${action.type}`);
@@ -778,29 +885,64 @@ const LogionChainContextProvider = (props: LogionChainContextProviderProps): JSX
}
}, [ state.tryEnableMetaMask, tryEnableMetaMask ]);
- const submitCall = useCallback(async (call: Call) => {
+ const submitCallBatch = useCallback(async (batch: CallBatch) => {
if(state.extrinsicSubmissionState.canSubmit()) {
dispatch({
type: 'SUBMIT_EXTRINSIC',
+ submissions: batch.jobs.length,
});
- (async function() {
+ let errorMap: Record = {};
+ for(const job of batch.jobs) {
try {
- await call((callbackResult: ISubmittableResult) => dispatch({
+ dispatch({
+ type: 'SET_SUBMISSION_STARTED',
+ submissionId: job.submissionId,
+ });
+ await job.call((callbackResult: ISubmittableResult) => dispatch({
type: 'SET_SUBMITTABLE_RESULT',
result: callbackResult,
+ submissionId: job.submissionId,
}));
- dispatch({ type: 'SET_SUBMISSION_ENDED' });
+ dispatch({
+ type: 'SET_SUBMISSION_ENDED',
+ submissionId: job.submissionId,
+ });
} catch(e) {
console.log(e);
dispatch({
type: 'SET_SUBMISSION_ENDED',
extrinsicSubmissionError: e,
+ submissionId: job.submissionId,
});
+ errorMap[job.submissionId] = e;
}
- })();
+ }
+ return errorMap;
+ } else {
+ let errorMap: Record = {};
+ for(const job of batch.jobs) {
+ errorMap[job.submissionId] = "Could not submit";
+ }
+ return errorMap;
}
}, [ state.extrinsicSubmissionState ]);
+ useEffect(() => {
+ if(state.submitCallBatch !== submitCallBatch) {
+ dispatch({
+ type: 'SET_SUBMIT_CALL_BATCH',
+ submitCallBatch,
+ });
+ }
+ }, [ state.submitCallBatch, submitCallBatch ]);
+
+ const submitCall = useCallback(async (call: Call) => {
+ const errors = await submitCallBatch(CallBatch.fromSingle(call));
+ if(errors[CallBatch.DEFAULT_SUBMISSION_ID]) {
+ throw errors[CallBatch.DEFAULT_SUBMISSION_ID];
+ }
+ }, [ submitCallBatch ]);
+
useEffect(() => {
if(state.submitCall !== submitCall) {
dispatch({
diff --git a/src/logion-chain/__mocks__/LogionChainMock.ts b/src/logion-chain/__mocks__/LogionChainMock.ts
index a15ce8a6..e2437247 100644
--- a/src/logion-chain/__mocks__/LogionChainMock.ts
+++ b/src/logion-chain/__mocks__/LogionChainMock.ts
@@ -8,7 +8,7 @@ import Accounts, { Account } from 'src/common/types/Accounts';
import { LogionClient } from '@logion/client/dist/LogionClient.js';
import { LogionClient as LogionClientMock } from '../../__mocks__/LogionClientMock';
import { api } from "src/__mocks__/LogionMock";
-import { Call } from "../LogionChainContext";
+import { Call, CallBatch } from "../LogionChainContext";
export const LogionChainContextProvider = (props: any) => null;
@@ -104,7 +104,14 @@ export const SUCCESSFUL_SUBMISSION: unknown = {
canSubmit: () => true,
isSuccessful: () => true,
isError: () => false,
+ isInProgress: () => false,
error: null,
+ getError: () => null,
+ getResult: () => ({
+ status: {
+ isFinalized: true,
+ }
+ }),
result: {
status: {
isFinalized: true,
@@ -115,10 +122,17 @@ export const SUCCESSFUL_SUBMISSION: unknown = {
};
export const FAILED_SUBMISSION: unknown = {
- canSubmit: () => false,
+ canSubmit: () => true,
isSuccessful: () => false,
isError: () => true,
+ isInProgress: () => false,
error: "error",
+ getError: () => "error",
+ getResult: () => ({
+ status: {
+ isFinalized: false,
+ }
+ }),
result: {
status: {
isFinalized: false,
@@ -132,7 +146,14 @@ export const PENDING_SUBMISSION: unknown = {
canSubmit: () => false,
isSuccessful: () => false,
isError: () => false,
+ isInProgress: () => true,
error: null,
+ getError: () => null,
+ getResult: () => ({
+ status: {
+ isFinalized: false,
+ }
+ }),
result: {
status: {
isFinalized: false,
@@ -173,6 +194,7 @@ export function useLogionChain() {
resetSubmissionState: () => {},
submitSignAndSubmit: () => {},
submitCall: (call: Call) => call(() => {}),
+ submitCallBatch: (batch: CallBatch) => { batch.jobs.forEach(job => job.call(() => {})); return {}; },
clearSubmissionState: () => {},
};
}
|