diff --git a/src/ui/pages/History/ActionDetailedView/ActionDetailedView.tsx b/src/ui/pages/History/ActionDetailedView/ActionDetailedView.tsx
index 3ef87a114..0544ec520 100644
--- a/src/ui/pages/History/ActionDetailedView/ActionDetailedView.tsx
+++ b/src/ui/pages/History/ActionDetailedView/ActionDetailedView.tsx
@@ -13,6 +13,7 @@ import { RateLine } from './components/RateLine';
import { SenderReceiverLine } from './components/SenderReceiverLine';
import { FeeLine } from './components/FeeLine';
import { ExplorerInfo } from './components/ExplorerInfo';
+import { LocalActionView } from './components/LocalActionView';
const dateFormatter = new Intl.DateTimeFormat('en', {
year: 'numeric',
@@ -97,6 +98,11 @@ export function ActionDetailedView({
) : null}
) : null}
+
diff --git a/src/ui/pages/History/ActionDetailedView/components/LocalActionView.tsx b/src/ui/pages/History/ActionDetailedView/components/LocalActionView.tsx
new file mode 100644
index 000000000..a683505a3
--- /dev/null
+++ b/src/ui/pages/History/ActionDetailedView/components/LocalActionView.tsx
@@ -0,0 +1,137 @@
+import React, { useMemo } from 'react';
+import type { ActionTransfers } from 'defi-sdk';
+import { useAssetsPrices } from 'defi-sdk';
+import { useStore } from '@store-unit/react';
+import type { SwapLocalAction } from 'src/ui/transactions/local-actions-store';
+import {
+ localActionsStore,
+ LocalActionsStore,
+ type LocalAction,
+} from 'src/ui/transactions/local-actions-store';
+import { Button } from 'src/ui/ui-kit/Button';
+import { UnstyledLink } from 'src/ui/ui-kit/UnstyledLink';
+import { VStack } from 'src/ui/ui-kit/VStack';
+import RetryIcon from 'jsx:src/ui/assets/actions/swap.svg';
+import { HStack } from 'src/ui/ui-kit/HStack';
+import { UIText } from 'src/ui/ui-kit/UIText';
+import { createChain } from 'src/modules/networks/Chain';
+import { useCurrency } from 'src/modules/currency/useCurrency';
+import { commonToBase } from 'src/shared/units/convert';
+import { getDecimals } from 'src/modules/networks/asset';
+import type { ClientTransactionStatus } from 'src/modules/ethereum/transactions/addressAction';
+import { TransferInfo } from './TransferInfo';
+
+function SwapLocalActionView({
+ localAction,
+ showTransferInfo,
+ status,
+}: {
+ localAction: SwapLocalAction;
+ showTransferInfo?: boolean;
+ status: ClientTransactionStatus;
+}) {
+ const link = LocalActionsStore.getActionLink(localAction);
+ const chain = createChain(localAction.chain);
+ const { currency } = useCurrency();
+ const { value, isLoading } = useAssetsPrices({
+ asset_codes: [localAction.spendTokenId, localAction.receiveTokenId],
+ currency,
+ });
+
+ const transfers = useMemo(() => {
+ const spendToken = value?.[localAction.spendTokenId];
+ const receiveToken = value?.[localAction.receiveTokenId];
+ return {
+ incoming: receiveToken
+ ? [
+ {
+ asset: { fungible: receiveToken },
+ quantity: commonToBase(
+ localAction.receiveInput,
+ getDecimals({ asset: receiveToken, chain })
+ ).toFixed(),
+ price: receiveToken.price?.value || null,
+ },
+ ]
+ : [],
+ outgoing: spendToken
+ ? [
+ {
+ asset: { fungible: spendToken },
+ quantity: commonToBase(
+ localAction.spendInput,
+ getDecimals({ asset: spendToken, chain })
+ ).toFixed(),
+ price: spendToken.price?.value || null,
+ },
+ ]
+ : [],
+ };
+ }, [value, chain, localAction]);
+
+ const isFailed = status === 'failed' || status === 'dropped';
+
+ if (isLoading) {
+ return null;
+ }
+
+ return (
+
+ {showTransferInfo ? (
+
+ {transfers.outgoing ? (
+
+ ) : null}
+ {transfers.incoming ? (
+
+ ) : null}
+
+ ) : null}
+ {link ? (
+
+ ) : null}
+
+ );
+}
+
+export function LocalActionView({
+ localActionKey,
+ showTransferInfo,
+ status,
+}: {
+ localActionKey: string;
+ showTransferInfo?: boolean;
+ status: ClientTransactionStatus;
+}) {
+ const { localActions } = useStore(localActionsStore);
+
+ const localAction = localActions[localActionKey] as LocalAction | undefined;
+
+ if (localAction?.kind === 'swap') {
+ return (
+
+ );
+ }
+ return null;
+}
diff --git a/src/ui/pages/SwapForm/SuccessState/SuccessState.tsx b/src/ui/pages/SwapForm/SuccessState/SuccessState.tsx
index 99d896401..9a54b28ab 100644
--- a/src/ui/pages/SwapForm/SuccessState/SuccessState.tsx
+++ b/src/ui/pages/SwapForm/SuccessState/SuccessState.tsx
@@ -10,14 +10,19 @@ import { SuccessStateLoader } from 'src/ui/shared/forms/SuccessState/SuccessStat
import { SuccessStateToken } from 'src/ui/shared/forms/SuccessState/SuccessStateToken';
import { useActionStatusByHash } from 'src/ui/shared/forms/SuccessState/useActionStatusByHash';
import { NavigationTitle } from 'src/ui/components/NavigationTitle';
-import { GasbackDecorated } from '../../SendForm/SuccessState/SuccessState';
+import RetryIcon from 'jsx:src/ui/assets/actions/swap.svg';
+import { Button } from 'src/ui/ui-kit/Button';
+import { HStack } from 'src/ui/ui-kit/HStack';
+import { UIText } from 'src/ui/ui-kit/UIText';
import type { BareAddressPosition } from '../BareAddressPosition';
+import { GasbackDecorated } from '../../SendForm/SuccessState/SuccessState';
export function SuccessState({
swapFormState,
spendPosition,
receivePosition,
hash,
+ onRetry,
onDone,
gasbackValue,
}: {
@@ -26,6 +31,7 @@ export function SuccessState({
receivePosition: BareAddressPosition;
hash: string;
gasbackValue: number | null;
+ onRetry: () => void;
onDone: () => void;
}) {
const { networks } = useNetworks();
@@ -82,6 +88,16 @@ export function SuccessState({
) : null
}
+ secondaryAction={
+ actionStatus === 'failed' || actionStatus === 'dropped' ? (
+
+ ) : null
+ }
onDone={onDone}
/>
>
diff --git a/src/ui/pages/SwapForm/SwapForm.tsx b/src/ui/pages/SwapForm/SwapForm.tsx
index 8d15efb45..8fa1c2898 100644
--- a/src/ui/pages/SwapForm/SwapForm.tsx
+++ b/src/ui/pages/SwapForm/SwapForm.tsx
@@ -82,6 +82,7 @@ import type { ZerionApiClient } from 'src/modules/zerion-api/zerion-api-bare';
import { useGasbackEstimation } from 'src/modules/ethereum/account-abstraction/rewards';
import { HiddenValidationInput } from 'src/ui/shared/forms/HiddenValidationInput';
import { getNetworksStore } from 'src/modules/networks/networks-store.client';
+import { localActionsStore } from 'src/ui/transactions/local-actions-store';
import {
DEFAULT_CONFIGURATION,
applyConfiguration,
@@ -303,7 +304,10 @@ export function SwapFormComponent() {
: null;
const {
enough_allowance,
- allowanceQuery: { refetch: refetchAllowanceQuery },
+ allowanceQuery: {
+ refetch: refetchAllowanceQuery,
+ remove: removeAllowanceQuery,
+ },
approvalTransactionQuery: { isFetching: approvalTransactionIsFetching },
approvalTransaction,
} = useApproveHandler({
@@ -447,8 +451,8 @@ export function SwapFormComponent() {
}, [
approveTxStatus,
refetchAllowanceQuery,
- refetchNonce,
refetchQuotes,
+ refetchNonce,
resetApproveMutation,
]);
@@ -506,6 +510,35 @@ export function SwapFormComponent() {
onBeforeSubmit();
return 'sendTransaction';
},
+ onSuccess: (hash) => {
+ if (!snapshotRef.current) {
+ return;
+ }
+ const {
+ chainInput,
+ receiveInput,
+ spendInput,
+ receiveTokenInput,
+ spendTokenInput,
+ } = snapshotRef.current;
+ if (
+ !chainInput ||
+ !spendInput ||
+ !receiveInput ||
+ !spendTokenInput ||
+ !receiveTokenInput
+ ) {
+ return;
+ }
+ localActionsStore.saveAction(hash, {
+ kind: 'swap',
+ chain: chainInput,
+ receiveInput,
+ spendInput,
+ spendTokenId: spendTokenInput,
+ receiveTokenId: receiveTokenInput,
+ });
+ },
});
const resetMutationIfNotLoading = useEvent(() => {
@@ -568,6 +601,15 @@ export function SwapFormComponent() {
receivePosition={receivePosition}
swapFormState={snapshotRef.current}
gasbackValue={gasbackValueRef.current}
+ onRetry={() => {
+ reset();
+ removeAllowanceQuery();
+ refetchNonce();
+ refetchQuotes();
+ snapshotRef.current = null;
+ feeValueCommonRef.current = null;
+ gasbackValueRef.current = null;
+ }}
onDone={() => {
reset();
snapshotRef.current = null;
diff --git a/src/ui/shared/forms/SuccessState/SuccessStateLoader.tsx b/src/ui/shared/forms/SuccessState/SuccessStateLoader.tsx
index 14c8f65f5..ebe276727 100644
--- a/src/ui/shared/forms/SuccessState/SuccessStateLoader.tsx
+++ b/src/ui/shared/forms/SuccessState/SuccessStateLoader.tsx
@@ -181,6 +181,7 @@ export function SuccessStateLoader({
explorerUrl,
error,
confirmedContent,
+ secondaryAction,
onDone,
}: {
startItem: React.ReactNode;
@@ -193,6 +194,7 @@ export function SuccessStateLoader({
explorerUrl?: string;
error?: string;
confirmedContent?: React.ReactNode;
+ secondaryAction?: React.ReactNode;
onDone?: () => void;
}) {
const showLongWaitNotice = useRenderDelay(5000);
@@ -309,9 +311,12 @@ export function SuccessStateLoader({
) : null}
-
+
+ {secondaryAction}
+
+
diff --git a/src/ui/transactions/local-actions-store.ts b/src/ui/transactions/local-actions-store.ts
new file mode 100644
index 000000000..142dfdc6c
--- /dev/null
+++ b/src/ui/transactions/local-actions-store.ts
@@ -0,0 +1,71 @@
+import { PersistentStore } from 'src/modules/persistent-store';
+
+type BaseLocalAction = {
+ datetime: number;
+};
+
+export type SendLocalAction = {
+ kind: 'send';
+ recepient: string;
+ chain: string;
+ amount: string;
+ tokenId: string;
+};
+
+export type SwapLocalAction = {
+ kind: 'swap';
+ chain: string;
+ spendTokenId: string;
+ spendInput: string;
+ receiveTokenId: string;
+ receiveInput: string;
+};
+
+// TODO add bridge actions when merged
+
+export type LocalAction = SendLocalAction | SwapLocalAction;
+type State = {
+ version: 1;
+ localActions: Record;
+};
+
+export class LocalActionsStore extends PersistentStore {
+ static getActionLink(action: LocalAction) {
+ if (action.kind === 'swap') {
+ const searchParams = new URLSearchParams({
+ chainInput: action.chain,
+ spendTokenInput: action.spendTokenId,
+ spendInput: action.spendInput,
+ receiveTokenInput: action.receiveTokenId,
+ });
+ return `/swap-form/?${searchParams.toString()}`;
+ }
+ if (action.kind === 'send') {
+ const searchParams = new URLSearchParams({
+ addressInputValue: action.recepient,
+ tokenValue: action.amount,
+ tokenAssetCode: action.tokenId,
+ });
+ return `/send-form/?${searchParams.toString()}`;
+ }
+ return null;
+ }
+
+ saveAction(key: string, action: LocalAction) {
+ this.setState((state) => ({
+ ...state,
+ localActions: {
+ ...state.localActions,
+ [key]: {
+ ...action,
+ datetime: new Date().getTime(),
+ },
+ },
+ }));
+ }
+}
+
+export const localActionsStore = new LocalActionsStore(
+ { version: 1, localActions: {} },
+ 'localActions'
+);