Skip to content

Commit 0f4f046

Browse files
feat: improve error message on wasm errors (#2025)
1 parent 17f9cce commit 0f4f046

File tree

16 files changed

+254
-66
lines changed

16 files changed

+254
-66
lines changed

apps/namadillo/src/App/Common/Toast.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,7 @@ const Toast = ({ notification, onClose }: ToastProps): JSX.Element => {
117117
>
118118
<strong className="block text-sm">{notification.title}</strong>
119119
<div className="leading-tight text-xs">{notification.description}</div>
120-
{notification.type !== "partialSuccess" &&
121-
notification.type !== "error" &&
120+
{notification.type !== "error" &&
122121
notification.details &&
123122
!viewDetails && (
124123
<button
@@ -129,9 +128,7 @@ const Toast = ({ notification, onClose }: ToastProps): JSX.Element => {
129128
</button>
130129
)}
131130
{notification.details &&
132-
(viewDetails ||
133-
notification.type === "partialSuccess" ||
134-
notification.type === "error") && (
131+
(viewDetails || notification.type === "error") && (
135132
<div className="w-full text-xs text-white block">
136133
{notification.details}
137134
</div>

apps/namadillo/src/App/Staking/ClaimRewardsSubmitModalStage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type ClaimRewardsPanelProps = {
1818
rewardsToClaim: ClaimRewardsMsgValue[];
1919
isClaimAndStake: boolean;
2020
feeProps: TransactionFeeProps;
21-
onClaim: () => void;
21+
onClaim: () => Promise<void>;
2222
isClaiming: boolean;
2323
isEnabled: boolean;
2424
error?: string;

apps/namadillo/src/App/Staking/IncrementBonding.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ const IncrementBonding = (): JSX.Element => {
115115
return getTopValidatorsAddresses(validators?.data ?? []);
116116
}, [validators]);
117117

118-
const onSubmit = (e: React.FormEvent): void => {
118+
const onSubmit = async (e: React.FormEvent): Promise<void> => {
119119
e.preventDefault();
120-
performBonding();
120+
await performBonding();
121121
};
122122

123123
const errorMessage = ((): string => {

apps/namadillo/src/App/Staking/ReDelegate.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const ReDelegate = (): JSX.Element => {
8787
});
8888
};
8989

90-
const onSubmit = (e: React.FormEvent): void => {
90+
const onSubmit = async (e: React.FormEvent): Promise<void> => {
9191
e.preventDefault();
9292
// Go to next page or do nothing
9393
if (step === "remove" && totalToRedelegate) {
@@ -96,7 +96,7 @@ export const ReDelegate = (): JSX.Element => {
9696
}
9797
return;
9898
}
99-
performRedelegate();
99+
await performRedelegate();
100100
};
101101

102102
const totalAssignedAmounts = BigNumber.sum(

apps/namadillo/src/App/Staking/StakingRewards.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ export const StakingRewards = (): JSX.Element => {
9191
return "Confirm Claim";
9292
}, [shouldClaimAndStake, rewardsToClaim]);
9393

94-
const onSubmitClaim = (): void => {
94+
const onSubmitClaim = async (): Promise<void> => {
9595
if (shouldClaimAndStake) {
96-
claimRewardsAndStake();
96+
await claimRewardsAndStake();
9797
} else {
98-
claimRewards();
98+
await claimRewards();
9999
}
100100
};
101101

apps/namadillo/src/App/Staking/StakingWithdrawModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export const StakingWithdrawModal = (): JSX.Element => {
108108
<ActionButton
109109
outlineColor="pink"
110110
textColor="pink"
111-
onClick={() => withdraw()}
111+
onClick={async () => await withdraw()}
112112
disabled={
113113
totalWithdrawableAmount.eq(0) ||
114114
!withdrawTxEnabled ||

apps/namadillo/src/App/Staking/Unstake.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ export const Unstake = (): JSX.Element => {
8282
);
8383
};
8484

85-
const onSubmit = (e: FormEvent): void => {
85+
const onSubmit = async (e: FormEvent): Promise<void> => {
8686
e.preventDefault();
87-
performUnbond();
87+
await performUnbond();
8888
};
8989

9090
const validationMessage = ((): string => {

apps/namadillo/src/hooks/useTransaction.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,15 @@ export const useTransaction = <T,>({
8181
const { mutateAsync: performBuildTx } = useAtomValue(createTxAtom);
8282

8383
// Claim & Stake is the only array of tx kinds. The rest are single tx kinds.
84+
const arr = new Array(Math.max(1, params.length));
8485
const kinds =
8586
Array.isArray(eventType) ?
86-
[...eventType]
87-
: new Array(Math.max(1, params.length)).fill(eventType); // Don't display zeroed value when params are not set yet.
87+
// **IMPORTANT**
88+
// If eventType is an array, we set kinds by multiplying each kind by params length
89+
// i.e. (["ClaimRewards", "Bond"] AND params.length == 2) => ["ClaimRewards", "Bond", "ClaimRewards", "Bond"]
90+
arr.fill(eventType).flat()
91+
: arr.fill(eventType); // Don't display zeroed value when params are not set yet.
92+
8893
const feeProps = useTransactionFee(
8994
kinds,
9095
kinds.some((k) => ["ShieldedTransfer", "UnshieldingTransfer"].includes(k))

apps/namadillo/src/hooks/useTransactionNotifications.tsx

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Stack } from "@namada/components";
2-
import { RedelegateMsgValue, TxProps } from "@namada/types";
2+
import { ClaimRewardsProps, RedelegateMsgValue, TxProps } from "@namada/types";
33
import { mapUndefined, shortenAddress } from "@namada/utils";
44
import { NamCurrency } from "App/Common/NamCurrency";
55
import { TokenCurrency } from "App/Common/TokenCurrency";
@@ -34,6 +34,25 @@ const parseTxsData = <T extends TxWithAmount>(
3434
return { total, id };
3535
};
3636

37+
const getClaimingFailedDetails = (
38+
data: { error?: string; value: ClaimRewardsProps }[]
39+
): React.ReactNode => {
40+
const withErrors = data.filter((d) => typeof d.error === "string");
41+
return (
42+
<Stack>
43+
<Stack as="ul" gap={1}>
44+
{withErrors.map((d, idx) => {
45+
return (
46+
<li className="flex justify-between" key={idx}>
47+
<b>{d.error}</b>
48+
</li>
49+
);
50+
})}
51+
</Stack>
52+
</Stack>
53+
);
54+
};
55+
3756
const getAmountByValidatorList = <T extends AmountByValidator>(
3857
data: T[]
3958
): React.ReactNode => {
@@ -51,6 +70,25 @@ const getAmountByValidatorList = <T extends AmountByValidator>(
5170
);
5271
};
5372

73+
const getAmountByValidatorListWithErr = <T extends AmountByValidator>(
74+
data: { value: T; error?: string }[]
75+
): React.ReactNode => {
76+
return (
77+
<Stack gap={2}>
78+
{getAmountByValidatorList(data.map((d) => d.value))}
79+
<Stack as="ul" gap={1}>
80+
{data.map((d) => {
81+
return (
82+
<li className="flex justify-between" key={d.value.validator}>
83+
<b>{d.error}</b>
84+
</li>
85+
);
86+
})}
87+
</Stack>
88+
</Stack>
89+
);
90+
};
91+
5492
const getReDelegateDetailList = (
5593
data: RedelegateMsgValue[]
5694
): React.ReactNode => {
@@ -70,6 +108,25 @@ const getReDelegateDetailList = (
70108
);
71109
};
72110

111+
const getReDelegateDetailListWithErr = (
112+
data: { value: RedelegateMsgValue; error?: string }[]
113+
): React.ReactNode => {
114+
return (
115+
<Stack gap={2}>
116+
{getReDelegateDetailList(data.map((d) => d.value))}
117+
<Stack as="ul" gap={1}>
118+
{data.map((d) => {
119+
return (
120+
<li className="flex justify-between" key={d.value.sourceValidator}>
121+
<b>{d.error}</b>
122+
</li>
123+
);
124+
})}
125+
</Stack>
126+
</Stack>
127+
);
128+
};
129+
73130
const partialSuccessDetails = (detail: {
74131
details: React.ReactNode;
75132
failedDescription: React.ReactNode;
@@ -112,7 +169,7 @@ export const useTransactionNotifications = (): void => {
112169
),
113170
details:
114171
e.detail.failedData ?
115-
failureDetails(getAmountByValidatorList(e.detail.failedData))
172+
failureDetails(getAmountByValidatorListWithErr(e.detail.failedData))
116173
: e.detail.error?.message,
117174
});
118175
});
@@ -149,7 +206,7 @@ export const useTransactionNotifications = (): void => {
149206
failedDescription: (
150207
<>The following staking transactions were not applied:</>
151208
),
152-
failedDetails: getAmountByValidatorList(e.detail.failedData!),
209+
failedDetails: getAmountByValidatorListWithErr(e.detail.failedData!),
153210
}),
154211
type: "partialSuccess",
155212
});
@@ -185,7 +242,7 @@ export const useTransactionNotifications = (): void => {
185242
failedDescription: (
186243
<>The following unstaking transactions were not applied:</>
187244
),
188-
failedDetails: getAmountByValidatorList(e.detail.failedData!),
245+
failedDetails: getAmountByValidatorListWithErr(e.detail.failedData!),
189246
}),
190247
type: "partialSuccess",
191248
});
@@ -204,7 +261,7 @@ export const useTransactionNotifications = (): void => {
204261
),
205262
details:
206263
e.detail.failedData ?
207-
failureDetails(getAmountByValidatorList(e.detail.failedData))
264+
failureDetails(getAmountByValidatorListWithErr(e.detail.failedData!))
208265
: e.detail.error?.message,
209266
});
210267
});
@@ -243,7 +300,9 @@ export const useTransactionNotifications = (): void => {
243300
),
244301
details:
245302
e.detail.failedData ?
246-
failureDetails(getReDelegateDetailList(e.detail.failedData))
303+
failureDetails(
304+
getReDelegateDetailList(e.detail.failedData.map((fd) => fd.value))
305+
)
247306
: e.detail.error?.message,
248307
type: "error",
249308
});
@@ -279,7 +338,7 @@ export const useTransactionNotifications = (): void => {
279338
details: partialSuccessDetails({
280339
details: getReDelegateDetailList(e.detail.successData!),
281340
failedDescription: <>The following redelegations were not applied:</>,
282-
failedDetails: getReDelegateDetailList(e.detail.failedData!),
341+
failedDetails: getReDelegateDetailListWithErr(e.detail.failedData!),
283342
}),
284343
type: "partialSuccess",
285344
});
@@ -295,6 +354,30 @@ export const useTransactionNotifications = (): void => {
295354
});
296355
});
297356

357+
useTransactionEventListener("ClaimRewards.PartialSuccess", (e) => {
358+
const { tx } = e.detail;
359+
const id = createNotificationId(tx.map((t) => t.hash));
360+
const successes = e.detail.successData?.length || 0;
361+
const failures = e.detail.failedData?.length || 0;
362+
dispatchNotification({
363+
id,
364+
title: <>Some claim rewards transactions were not successful</>,
365+
description: (
366+
<>
367+
Successful transactions: {successes}
368+
<br />
369+
Unsuccessful transactions: {failures}
370+
</>
371+
),
372+
details: partialSuccessDetails({
373+
details: <>Inner transaction failed</>,
374+
failedDescription: <></>,
375+
failedDetails: getClaimingFailedDetails(e.detail.failedData!),
376+
}),
377+
type: "partialSuccess",
378+
});
379+
});
380+
298381
useTransactionEventListener("ClaimRewards.Error", (e) => {
299382
const id = createNotificationId(e.detail.tx.map((t) => t.hash));
300383
dispatchNotification({

apps/namadillo/src/lib/query.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Sdk } from "@namada/sdk/web";
22
import {
33
Account,
44
AccountType,
5-
BatchTxResultMsgValue,
65
TxMsgValue,
76
TxProps,
87
TxResponseMsgValue,
@@ -18,7 +17,7 @@ import {
1817
TransactionEventsClasses,
1918
TransactionEventsStatus,
2019
} from "types/events";
21-
import { toErrorDetail } from "utils";
20+
import { textToErrorDetail, toErrorDetail } from "utils";
2221
import { getSdkInstance } from "utils/sdk";
2322

2423
export type TransactionPair<T> = {
@@ -199,10 +198,16 @@ export const signEncodedTx = async <T>(
199198
export const broadcastTransaction = async <T>(
200199
encodedTx: EncodedTxData<T>,
201200
signedTxs: Uint8Array[]
202-
): Promise<PromiseSettledResult<TxResponseMsgValue>[]> => {
201+
): Promise<PromiseSettledResult<[EncodedTxData<T>, TxResponseMsgValue]>[]> => {
203202
const { rpc } = await getSdkInstance();
204203
const response = await Promise.allSettled(
205-
encodedTx.txs.map((_, i) => rpc.broadcastTx(signedTxs[i]))
204+
encodedTx.txs.map((_, i) =>
205+
rpc
206+
.broadcastTx(signedTxs[i])
207+
.then(
208+
(res) => [encodedTx, res] as [EncodedTxData<T>, TxResponseMsgValue]
209+
)
210+
)
206211
);
207212

208213
return response;
@@ -231,16 +236,16 @@ export const broadcastTxWithEvents = async <T>(
231236
.flat();
232237

233238
try {
234-
const commitments = results.map((result) => {
239+
const resolvedResults = results.map((result) => {
235240
if (result.status === "fulfilled") {
236-
return result.value.commitments;
241+
return result.value;
237242
} else {
238243
throw new Error(toErrorDetail(encodedTx.txs, result.reason));
239244
}
240245
});
241246

242247
const { status, successData, failedData } = parseTxAppliedErrors(
243-
commitments.flat(),
248+
resolvedResults,
244249
hashes,
245250
data!
246251
);
@@ -274,36 +279,45 @@ export const broadcastTxWithEvents = async <T>(
274279
type TxAppliedResults<T> = {
275280
status: TransactionEventsStatus;
276281
successData?: T[];
277-
failedData?: T[];
282+
failedData?: { value: T; error?: string }[];
278283
};
279284

285+
type Hash = string;
286+
type Error = string | undefined;
280287
// Given an array of broadcasted Tx results,
281288
// collect any errors
282289
const parseTxAppliedErrors = <T>(
283-
results: BatchTxResultMsgValue[],
284-
txHashes: string[],
290+
results: [EncodedTxData<T>, TxResponseMsgValue][],
291+
txHashes: Hash[],
285292
data: T[]
286293
): TxAppliedResults<T> => {
287-
const txErrors: string[] = [];
294+
const txErrors: [Hash, Error][] = [];
288295
const dataWithHash = data?.map((d, i) => ({
289296
...d,
290297
hash: txHashes[i],
291298
}));
292299

293-
results.forEach((result) => {
294-
const { hash, isApplied } = result;
295-
if (!isApplied) {
296-
txErrors.push(hash);
297-
}
300+
results.forEach(([encodedTx, result]) => {
301+
result.commitments.forEach((batchTxResult) => {
302+
const { hash, isApplied, error } = batchTxResult;
303+
if (!isApplied) {
304+
txErrors.push([
305+
hash,
306+
error && textToErrorDetail(error, encodedTx.txs[0]),
307+
]);
308+
}
309+
});
298310
});
299311

300312
if (txErrors.length) {
301313
const successData = dataWithHash?.filter((data) => {
302-
return !txErrors.includes(data.hash);
314+
return !txErrors.find(([hash]) => hash === data.hash);
303315
});
304316

305-
const failedData = dataWithHash?.filter((data) => {
306-
return txErrors.includes(data.hash);
317+
// flatMap because js does not have filterMap
318+
const failedData = dataWithHash?.flatMap((data) => {
319+
const err = txErrors.find(([hash]) => hash === data.hash);
320+
return err ? [{ value: data, error: err[1] }] : [];
307321
});
308322

309323
if (successData?.length) {

0 commit comments

Comments
 (0)