Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions packages/widget/src/domain/types/external-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import type { RefObject } from "react";
import type { SKExternalProviders } from "./wallets";
import type { SKTx, SKTxMeta } from "./wallets/generic-wallet";

export class ExternalProviderError extends Error {
_tag = "ExternalProviderError";

constructor(
readonly customMessage: string | null,
cause?: unknown
) {
super(customMessage ?? "External provider failed", { cause });
}
}

export class ExternalProvider {
constructor(private variantProvider: RefObject<SKExternalProviders>) {}

Expand All @@ -13,8 +24,8 @@ export class ExternalProvider {
).toEither(new Error("Invalid provider type"))
)
.chain((_sendTransaction) =>
EitherAsync(() => _sendTransaction(tx, txMeta)).mapLeft(
() => new Error("Failed to send transaction, unknown error")
EitherAsync(() => _sendTransaction(tx, txMeta)).mapLeft((error) =>
toExternalProviderError(error)
)
)
.chain((res) => {
Expand All @@ -26,7 +37,9 @@ export class ExternalProvider {
return EitherAsync.liftEither(Right(res.txHash));
}

return EitherAsync.liftEither(Left(res.error));
return EitherAsync.liftEither(
Left(new ExternalProviderError(res.error))
);
});
}

Expand All @@ -44,7 +57,17 @@ export class ExternalProvider {
this.variantProvider.current.provider.signMessage(messageHash)
).mapLeft((e) => {
console.error(e);
return new Error("Failed to sign message");
return toExternalProviderError(e);
});
}
}

const toExternalProviderError = (error: unknown) =>
new ExternalProviderError(
error instanceof Error && error.message
? error.message
: typeof error === "string" && error
? error
: null,
error
);
12 changes: 11 additions & 1 deletion packages/widget/src/pages/steps/hooks/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ export class SignError extends Error {
_tag = "SignError";
txId: string;
network: string;
customMessage: string | null;

constructor({ network, txId }: { txId: string; network: string }) {
constructor({
customMessage = null,
network,
txId,
}: {
customMessage?: string | null;
txId: string;
network: string;
}) {
super();

this._tag = "SignError";
this.txId = txId;
this.network = network;
this.customMessage = customMessage;
}
}
export class GetStakeSessionError extends Error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type RefObject, useMemo, useState } from "react";
import { assign, emit, setup } from "xstate";
import { isTxError } from "../../../domain";
import type { ActionDto, TransactionDto } from "../../../domain/types/action";
import { ExternalProviderError } from "../../../domain/types/external-providers";
import type { ActionMeta } from "../../../domain/types/wallets/generic-wallet";
import { useTrackEvent } from "../../../hooks/tracking/use-track-event";
import { useSavedRef } from "../../../hooks/use-saved-ref";
Expand All @@ -25,7 +26,7 @@ type TxMeta = {
url: string | null;
signedTx: string | null;
broadcasted: boolean | null;
signError: SendTransactionError | TransactionDecodeError | null;
signError: SendTransactionError | TransactionDecodeError | SignError | null;
txCheckError: GetStakeSessionError | null;
done: boolean;
};
Expand Down Expand Up @@ -240,8 +241,12 @@ const getMachine = (
data: { signedTx: val, broadcasted: false },
}))
.mapLeft(
() =>
(error) =>
new SignError({
customMessage:
error instanceof ExternalProviderError
? error.customMessage
: null,
network: tx.network,
txId: tx.id,
})
Expand Down
15 changes: 15 additions & 0 deletions packages/widget/src/pages/steps/hooks/use-steps.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,20 @@ export const useSteps = ({
]
);

const customSignErrorMessage = useMemo(() => {
const error = machineState.context.currentTxMeta
.chainNullable((currentTxMeta) => {
return machineState.context.txStates[currentTxMeta.idx]?.meta.signError;
})
.extractNullable();

if (!error || !("customMessage" in error)) return null;

return typeof error.customMessage === "string" && error.customMessage
? error.customMessage
: null;
}, [machineState.context.currentTxMeta, machineState.context.txStates]);

const { t } = useTranslation();

const onClickRef = useSavedRef(onClick);
Expand All @@ -190,6 +204,7 @@ export const useSteps = ({
retry,
txStates,
cta,
customSignErrorMessage,
};
};

Expand Down
25 changes: 21 additions & 4 deletions packages/widget/src/pages/steps/pages/common.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useSettings } from "../../../providers/settings";
import { PageContainer } from "../../components/page-container";
import { PageCtaButton } from "../../components/page-cta";
import { useSteps } from "../hooks/use-steps.hook";
import { utilaPendingApprovalsBanner } from "./styles.css";
import { stepsErrorBanner, utilaPendingApprovalsBanner } from "./styles.css";
import { TxState } from "./tx-state";

type StepsPageProps = {
Expand All @@ -28,9 +28,9 @@ export const StepsPage = ({
onSignSuccess,
providersDetails,
}: StepsPageProps) => {
const { variant } = useSettings();
const { dashboardVariant, variant } = useSettings();

const { retry, txStates, cta } = useSteps({
const { retry, txStates, cta, customSignErrorMessage } = useSteps({
inputToken,
session,
onSignSuccess,
Expand All @@ -48,6 +48,19 @@ export const StepsPage = ({
<Heading variant={{ level: "h4" }}>{t("steps.title")}</Heading>
</Box>

{customSignErrorMessage && (
<Box
className={stepsErrorBanner}
data-rk="steps-custom-sign-error"
px="4"
py="3"
>
<Text variant={{ weight: "normal", type: "inverted" }}>
{customSignErrorMessage}
</Text>
</Box>
)}

{showUtilaPendingApprovals && (
<Box
className={utilaPendingApprovalsBanner}
Expand Down Expand Up @@ -88,7 +101,11 @@ export const StepsPage = ({

{retry && (
<Box my="4">
<Button data-rk="footer-button-primary" onClick={retry}>
<Button
data-rk="footer-button-primary"
onClick={retry}
variant={{ size: dashboardVariant ? "compact" : "regular" }}
>
{t("shared.retry")}
</Button>
</Box>
Expand Down
7 changes: 6 additions & 1 deletion packages/widget/src/pages/steps/pages/styles.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,10 @@ export const halfOpacityAfter = style({ ":after": { opacity: 0.5 } });

export const utilaPendingApprovalsBanner = style({
background: vars.color.warningBoxBackground,
borderRadius: "20px",
borderRadius: vars.borderRadius.baseContract.xl,
});

export const stepsErrorBanner = style({
background: vars.color.errorBoxBackground,
borderRadius: vars.borderRadius.baseContract.xl,
});
2 changes: 1 addition & 1 deletion packages/widget/src/pages/steps/pages/tx-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export const TxState = ({ txState, position, count, session }: Props) => {
<Text>{t("steps.approve")}</Text>
{txState.state === TxStateEnum.SIGN_ERROR ? (
<Text variant={{ type: "danger" }}>
{txState.meta.signError?.message || t("steps.approve_error")}
{t("steps.approve_error")}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this make non-custom signing errors less actionable for users? those cases now seem to collapse into the generic approve_error message

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

</Text>
) : (
<Text variant={{ type: "muted", weight: "normal" }}>
Expand Down
5 changes: 4 additions & 1 deletion packages/widget/src/providers/sk-wallet/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export class SafeFailedError extends Error {
export class SendTransactionError extends Error {
_tag = "SendTransactionError";

constructor(cause?: unknown) {
constructor(
cause?: unknown,
readonly customMessage: string | null = null
) {
super("Send transaction failed", { cause });

this._tag = "SendTransactionError";
Expand Down
10 changes: 8 additions & 2 deletions packages/widget/src/providers/sk-wallet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
isTonChain,
isTronChain,
} from "../../domain/types/chains";
import { ExternalProviderError } from "../../domain/types/external-providers";
import {
decodeAndPrepareEvmTransaction,
substratePayloadCodec,
Expand Down Expand Up @@ -337,7 +338,10 @@ export const SKWalletProvider = ({ children }: PropsWithChildren) => {
.mapLeft(
(e) =>
new SendTransactionError(
typeof e === "string" ? e : undefined
e,
e instanceof ExternalProviderError
? e.customMessage
: null
)
)
)
Expand Down Expand Up @@ -510,7 +514,9 @@ export const SKWalletProvider = ({ children }: PropsWithChildren) => {
})
.mapLeft((e) => {
console.log(e);
return new Error("sign failed");
return e instanceof ExternalProviderError
? e
: new Error("sign failed");
}),
[connectorDetails, signMessageAsync]
);
Expand Down
1 change: 1 addition & 0 deletions packages/widget/src/styles/tokens/colors/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const baseColorsContract = {
selectValidatorMultiDefaultBackground: "",

warningBoxBackground: "",
errorBoxBackground: "",

positionsClaimRewardsBackground: "",
positionsActionRequiredBackground: "",
Expand Down
2 changes: 2 additions & 0 deletions packages/widget/src/styles/tokens/colors/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export const lightThemeColors: typeof colorsContract = {
skeletonLoaderHighlight: vars.color.background,

warningBoxBackground: "#FFE9BD",
errorBoxBackground: sharedSemanticColors.status.danger,

stakeSectionBackground: "#f5f5f6",

Expand Down Expand Up @@ -221,6 +222,7 @@ export const darkThemeColors: typeof colorsContract = {
skeletonLoaderHighlight: "#333333",

warningBoxBackground: vars.color.backgroundMuted,
errorBoxBackground: sharedSemanticColors.status.danger,

stakeSectionBackground: "#282828",

Expand Down
2 changes: 1 addition & 1 deletion packages/widget/src/translation/English/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@
"skipped": "Skipped",
"pending_approvals": "Pending approvals",
"pending_approvals_desc": "Please open your wallet to review and securely sign the transaction",
"approve_error": "Something went wrong. Check if wallet is connected",
"approve_error": "Something went wrong",
"tx_type": {
"SWAP": "SWAP",
"DEPOSIT": "DEPOSIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/widget/src/translation/French/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@
"skipped": "Ignorée",
"pending_approvals": "Approbations en attente",
"pending_approvals_desc": "Ouvrez votre portefeuille pour vérifier et signer la transaction en toute sécurité",
"approve_error": "Une erreur s'est produite. Vérifiez si le portefeuille est bien connecté",
"approve_error": "Une erreur s'est produite",
"tx_type": {
"SWAP": "ÉCHANGE",
"DEPOSIT": "DÉPÔT",
Expand Down
64 changes: 64 additions & 0 deletions packages/widget/tests/use-cases/sk-wallet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SKApiClientProvider } from "../../src/providers/api/api-client-provider
import { SKQueryClientProvider } from "../../src/providers/query-client";
import { SettingsContextProvider } from "../../src/providers/settings";
import { SKWalletProvider, useSKWallet } from "../../src/providers/sk-wallet";
import { SendTransactionError } from "../../src/providers/sk-wallet/errors";
import { SolanaProvider } from "../../src/providers/solana";
import { TrackingContextProviderWithProps } from "../../src/providers/tracking";
import { WagmiConfigProvider } from "../../src/providers/wagmi/provider";
Expand Down Expand Up @@ -116,6 +117,69 @@ describe("SK Wallet", () => {
);
});

it("preserves custom external provider transaction errors", async ({
worker,
}) => {
const customMessage = "Transaction blocked by policy";
const switchChainSpy = vi.fn(async (_: number) => {});
const sendTransactionSpy = vi.fn(async () => ({
type: "error" as const,
error: customMessage,
}));

worker.use(
http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => {
await delay();
return HttpResponse.json([MiscNetworks.Solana]);
})
);

const solanaWallet = await renderHookWithExternalProvider({
type: "generic",
currentAddress: "9TCnDo7Txc5bC9SnE9iKsU5CyffLfeK4nrv1BFUmxkiJ",
currentChain: solana.id,
supportedChainIds: [solana.id],
provider: {
signMessage: async () => "hash",
switchChain: switchChainSpy,
sendTransaction: sendTransactionSpy,
},
});

await expect.poll(() => solanaWallet.result.current.isConnected).toBe(true);

const solanaRes = await solanaWallet.result.current.signTransaction({
network: "solana",
tx: "12345",
txMeta: {
txId: "",
actionId: "",
actionType: "STAKE",
txType: "APPROVAL",
amount: "100",
inputToken: {
address: "",
decimals: 0,
symbol: "",
name: "",
network: "solana",
},
structuredTransaction: null,
annotatedTransaction: null,
providersDetails: [],
},
ledgerHwAppId: null,
});

expect(solanaRes.isLeft()).toBe(true);

const error = solanaRes.extract() as SendTransactionError;

expect(error).toBeInstanceOf(SendTransactionError);
expect(error.message).toBe("Send transaction failed");
expect(error.customMessage).toBe(customMessage);
});

it("should work with ton external provider", async ({ worker }) => {
const switchChainSpy = vi.fn(async (_: number) => {});
const sendTransactionSpy = vi.fn(async (_: unknown) => "hash");
Expand Down