Skip to content

Commit c987dca

Browse files
committed
feat: handle partially successful claim rewards tx
1 parent fe9798f commit c987dca

File tree

8 files changed

+211
-60
lines changed

8 files changed

+211
-60
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/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) {

apps/namadillo/src/types/events.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {
22
BondProps,
3+
ClaimRewardsProps,
34
RedelegateProps,
45
TxProps,
56
UnbondProps,
67
VoteProposalProps,
78
WithdrawProps,
89
} from "@namada/types";
9-
import { ClaimRewardsProps, TransferTransactionData } from "types";
10+
import { TransferTransactionData } from "types";
1011
import { TxKind } from "types/txKind";
1112

1213
export type TransactionEventsClasses = Partial<TxKind>;
@@ -32,7 +33,7 @@ export interface EventData<T> extends CustomEvent {
3233
data: T[];
3334
// If event is for PartialSuccess, use the following:
3435
successData?: T[];
35-
failedData?: T[];
36+
failedData?: { value: T; error?: string }[];
3637
error?: Error;
3738
};
3839
}
@@ -51,6 +52,7 @@ declare global {
5152
"Withdraw.Success": EventData<WithdrawProps>;
5253
"Withdraw.Error": EventData<WithdrawProps>;
5354
"ClaimRewards.Success": EventData<ClaimRewardsProps>;
55+
"ClaimRewards.PartialSuccess": EventData<ClaimRewardsProps>;
5456
"ClaimRewards.Error": EventData<ClaimRewardsProps>;
5557
"VoteProposal.Success": EventData<VoteProposalProps>;
5658
"VoteProposal.Error": EventData<VoteProposalProps>;

apps/namadillo/src/utils/index.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ export const toBaseAmount = (
124124
return displayAmount.shiftedBy(displayUnit.exponent);
125125
};
126126

127-
const toGasMsg = (error: string, gasLimit: BigNumber): string => {
128-
return `${error.toString()} Please raise the Gas Amount above the previously provided ${gasLimit} in the fee options for your transaction.`;
127+
export const toGasMsg = (gasLimit: BigNumber): string => {
128+
return `Please raise the Gas Amount above the previously provided ${gasLimit} in the fee options for your transaction.`;
129129
};
130130

131131
/**
@@ -141,13 +141,10 @@ export const toErrorDetail = (
141141
// TODO: Over time we may expand this to format errors for more result codes
142142
switch (code) {
143143
case ResultCode.TxGasLimit:
144-
return toGasMsg(error.toString(), args.gasLimit);
144+
return `${error.toString()} ${toGasMsg(args.gasLimit)}`;
145145
case ResultCode.WasmRuntimeError:
146146
// We can only check error type by reading the error message
147-
if (info.includes("Gas error:")) {
148-
return toGasMsg(error.toString(), args.gasLimit);
149-
}
150-
return error.toString() + ` ${info}`;
147+
return error.toString() + ` ${textToErrorDetail(info, tx[0])}`;
151148

152149
default:
153150
return error.toString() + ` ${info}`;
@@ -156,3 +153,13 @@ export const toErrorDetail = (
156153
return `${error.toString()}`;
157154
}
158155
};
156+
157+
export const textToErrorDetail = (text: string, tx: TxMsgValue): string => {
158+
const { args } = tx;
159+
160+
if (text.includes("Gas error:")) {
161+
return toGasMsg(args.gasLimit);
162+
} else {
163+
return text;
164+
}
165+
};

0 commit comments

Comments
 (0)