@keplr-wallet/background 0.13.34 → 0.13.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/build/keyring/service.js +31 -42
  2. package/build/keyring/service.js.map +1 -1
  3. package/build/keyring/service.spec.d.ts +1 -0
  4. package/build/keyring/service.spec.js +106 -0
  5. package/build/keyring/service.spec.js.map +1 -0
  6. package/build/tx-executor/index.d.ts +1 -0
  7. package/build/tx-executor/index.js +1 -0
  8. package/build/tx-executor/index.js.map +1 -1
  9. package/build/tx-executor/service.d.ts +8 -5
  10. package/build/tx-executor/service.js +56 -11
  11. package/build/tx-executor/service.js.map +1 -1
  12. package/build/tx-executor/types.d.ts +20 -1
  13. package/build/tx-executor/utils/diagnostics.d.ts +14 -0
  14. package/build/tx-executor/utils/diagnostics.js +215 -0
  15. package/build/tx-executor/utils/diagnostics.js.map +1 -0
  16. package/build/tx-executor/utils/diagnostics.spec.d.ts +1 -0
  17. package/build/tx-executor/utils/diagnostics.spec.js +48 -0
  18. package/build/tx-executor/utils/diagnostics.spec.js.map +1 -0
  19. package/build/tx-executor/utils/evm.js +7 -3
  20. package/build/tx-executor/utils/evm.js.map +1 -1
  21. package/build/tx-executor/utils/evm.spec.d.ts +1 -0
  22. package/build/tx-executor/utils/evm.spec.js +59 -0
  23. package/build/tx-executor/utils/evm.spec.js.map +1 -0
  24. package/package.json +13 -13
  25. package/src/keyring/service.spec.ts +141 -0
  26. package/src/keyring/service.ts +47 -59
  27. package/src/tx-executor/index.ts +1 -0
  28. package/src/tx-executor/service.ts +72 -17
  29. package/src/tx-executor/types.ts +42 -1
  30. package/src/tx-executor/utils/diagnostics.spec.ts +89 -0
  31. package/src/tx-executor/utils/diagnostics.ts +298 -0
  32. package/src/tx-executor/utils/evm.spec.ts +62 -0
  33. package/src/tx-executor/utils/evm.ts +18 -1
@@ -1,4 +1,4 @@
1
- import { KVStore } from "@keplr-wallet/common";
1
+ import { EventBusSubscriber, KVStore } from "@keplr-wallet/common";
2
2
  import { ChainsService } from "../chains";
3
3
  import { KeyRingCosmosService } from "../keyring-cosmos";
4
4
  import { KeyRingEthereumService } from "../keyring-ethereum";
@@ -20,6 +20,8 @@ import {
20
20
  TxExecutionResult,
21
21
  PendingTxExecutionResult,
22
22
  IBCSwapMinimalTrackingData,
23
+ TxExecutionDiagnostics,
24
+ TxExecutionEvent,
23
25
  } from "./types";
24
26
  import {
25
27
  action,
@@ -49,8 +51,12 @@ import {
49
51
  } from "./utils/cosmos";
50
52
  import { fillUnsignedEVMTx } from "./utils/evm";
51
53
  import { fetchWithRetry } from "./utils/fetch";
52
- import { EventBusSubscriber } from "@keplr-wallet/common";
53
- import { TxExecutionEvent } from "./types";
54
+ import { getTxExecutionDiagnostics } from "./utils/diagnostics";
55
+
56
+ type TraceTxResult = {
57
+ confirmed: boolean;
58
+ diagnostics?: TxExecutionDiagnostics;
59
+ };
54
60
 
55
61
  export class BackgroundTxExecutorService {
56
62
  @observable
@@ -418,6 +424,7 @@ export class BackgroundTxExecutorService {
418
424
  return {
419
425
  status: TxExecutionStatus.FAILED,
420
426
  error: result.error,
427
+ diagnostics: result.diagnostics,
421
428
  };
422
429
  }
423
430
  case BackgroundTxStatus.BLOCKED: {
@@ -507,6 +514,7 @@ export class BackgroundTxExecutorService {
507
514
  status: BackgroundTxStatus.FAILED,
508
515
  txHash,
509
516
  error: e?.message || "Transaction signing failed",
517
+ diagnostics: getTxExecutionDiagnostics(e),
510
518
  };
511
519
  }
512
520
  }
@@ -531,9 +539,9 @@ export class BackgroundTxExecutorService {
531
539
  // trace the tx
532
540
  try {
533
541
  const txWithHash = { ...tx, txHash };
534
- const confirmed = await this.traceTx(txWithHash);
542
+ const traceResult = await this.traceTx(txWithHash);
535
543
 
536
- if (confirmed) {
544
+ if (traceResult.confirmed) {
537
545
  return { status: BackgroundTxStatus.CONFIRMED, txHash };
538
546
  }
539
547
 
@@ -541,6 +549,7 @@ export class BackgroundTxExecutorService {
541
549
  status: BackgroundTxStatus.FAILED,
542
550
  txHash,
543
551
  error: "Transaction confirmation failed",
552
+ diagnostics: traceResult.diagnostics,
544
553
  };
545
554
  } catch (e) {
546
555
  console.error(`[TxExecutor] tx trace failed:`, e);
@@ -548,6 +557,12 @@ export class BackgroundTxExecutorService {
548
557
  status: BackgroundTxStatus.FAILED,
549
558
  txHash,
550
559
  error: e?.message || "Transaction confirmation failed",
560
+ diagnostics: getTxExecutionDiagnostics(e) ?? {
561
+ confirmation_failure_reason:
562
+ tx.type === BackgroundTxType.EVM
563
+ ? "evm_trace_error"
564
+ : "cosmos_trace_error",
565
+ },
551
566
  };
552
567
  }
553
568
  }
@@ -820,7 +835,7 @@ export class BackgroundTxExecutorService {
820
835
  return Buffer.from(txHash).toString("hex");
821
836
  }
822
837
 
823
- protected async traceTx(tx: BackgroundTx): Promise<boolean> {
838
+ protected async traceTx(tx: BackgroundTx): Promise<TraceTxResult> {
824
839
  switch (tx.type) {
825
840
  case BackgroundTxType.EVM: {
826
841
  return this.traceEvmTx(tx);
@@ -834,7 +849,7 @@ export class BackgroundTxExecutorService {
834
849
  }
835
850
  }
836
851
 
837
- private async traceEvmTx(tx: EVMBackgroundTx): Promise<boolean> {
852
+ private async traceEvmTx(tx: EVMBackgroundTx): Promise<TraceTxResult> {
838
853
  if (!tx.txHash) {
839
854
  throw new KeplrError("direct-tx-executor", 133, "Tx hash not found");
840
855
  }
@@ -851,55 +866,95 @@ export class BackgroundTxExecutorService {
851
866
  tx.txHash
852
867
  );
853
868
  if (!txReceipt) {
854
- return false;
869
+ return {
870
+ confirmed: false,
871
+ diagnostics: {
872
+ confirmation_failure_reason: "evm_receipt_missing",
873
+ },
874
+ };
855
875
  }
856
876
 
857
- return txReceipt.status === EthTxStatus.Success;
877
+ if (txReceipt.status === EthTxStatus.Success) {
878
+ return { confirmed: true };
879
+ }
880
+
881
+ return {
882
+ confirmed: false,
883
+ diagnostics: {
884
+ confirmation_failure_reason: "evm_receipt_status_failed",
885
+ },
886
+ };
858
887
  }
859
888
 
860
- private async traceCosmosTx(tx: CosmosBackgroundTx): Promise<boolean> {
889
+ private async traceCosmosTx(tx: CosmosBackgroundTx): Promise<TraceTxResult> {
861
890
  if (!tx.txHash) {
862
891
  throw new KeplrError("direct-tx-executor", 133, "Tx hash not found");
863
892
  }
864
893
 
865
894
  let txResult: any;
895
+ let traceErrored = false;
866
896
  try {
867
897
  txResult = await this.backgroundTxService.traceTx(tx.chainId, tx.txHash);
868
898
  } catch {
899
+ traceErrored = true;
869
900
  // WS retry 모두 실패 — REST fallback 시도
870
901
  }
871
902
 
872
903
  // WS에서 결과를 못 받은 경우, REST로 tx 존재 여부 확인
873
904
  // (tx가 온체인 성공했는데 WS 불안정으로 확인 못한 false positive 대응)
874
905
  if (!txResult) {
875
- txResult = await this.queryTxByRestFallback(tx.chainId, tx.txHash);
906
+ const restFallback = await this.queryTxByRestFallback(
907
+ tx.chainId,
908
+ tx.txHash
909
+ );
910
+ if (restFallback.failed) {
911
+ return {
912
+ confirmed: false,
913
+ diagnostics: {
914
+ confirmation_failure_reason: "cosmos_rest_fallback_missing",
915
+ },
916
+ };
917
+ }
918
+ txResult = restFallback.txResult;
876
919
  }
877
920
 
878
921
  if (!txResult) {
879
- return false;
922
+ return {
923
+ confirmed: false,
924
+ diagnostics: {
925
+ confirmation_failure_reason: traceErrored
926
+ ? "cosmos_trace_error"
927
+ : "cosmos_trace_missing",
928
+ },
929
+ };
880
930
  }
881
931
 
882
932
  // Tendermint/CometBFT omits the code field when tx is successful (code=0)
883
933
  // If code is present and non-zero, it's a failure
884
934
  if (txResult.code != null && txResult.code !== 0) {
885
- return false;
935
+ return {
936
+ confirmed: false,
937
+ diagnostics: {
938
+ confirmation_failure_reason: "cosmos_code_nonzero",
939
+ },
940
+ };
886
941
  }
887
942
 
888
- return true;
943
+ return { confirmed: true };
889
944
  }
890
945
 
891
946
  private async queryTxByRestFallback(
892
947
  chainId: string,
893
948
  txHash: string
894
- ): Promise<{ code?: number } | undefined> {
949
+ ): Promise<{ txResult?: { code?: number }; failed: boolean }> {
895
950
  try {
896
951
  const chainInfo = this.chainsService.getChainInfoOrThrow(chainId);
897
952
  const { data } = await fetchWithRetry<{
898
953
  tx_response?: { code?: number; txhash?: string };
899
954
  }>(chainInfo.rest, `/cosmos/tx/v1beta1/txs/${txHash}`);
900
- return data.tx_response;
955
+ return { txResult: data.tx_response, failed: false };
901
956
  } catch {
902
- return undefined;
957
+ return { failed: true };
903
958
  }
904
959
  }
905
960
 
@@ -1,5 +1,5 @@
1
1
  import { UnsignedTransaction } from "@ethersproject/transactions";
2
- import { StdFee } from "@keplr-wallet/types";
2
+ import { EvmGasSimulationOutcome, StdFee } from "@keplr-wallet/types";
3
3
  import { Any } from "@keplr-wallet/proto-types/google/protobuf/any";
4
4
  import { Msg } from "@keplr-wallet/types";
5
5
  import { SwapV2HistoryData } from "../recent-send-history";
@@ -94,6 +94,45 @@ export enum TxExecutionType {
94
94
 
95
95
  export type BackgroundTxFeeType = "low" | "average" | "high";
96
96
 
97
+ export type TxConfirmationFailureReason =
98
+ | "evm_receipt_missing"
99
+ | "evm_receipt_status_failed"
100
+ | "evm_trace_error"
101
+ | "cosmos_trace_missing"
102
+ | "cosmos_trace_error"
103
+ | "cosmos_rest_fallback_missing"
104
+ | "cosmos_code_nonzero";
105
+
106
+ export type GasEstimateContext =
107
+ | "review_simulation"
108
+ | "pre_sign_simulation"
109
+ | "background_fill_unsigned_tx";
110
+
111
+ export type GasEstimateErrorCategory =
112
+ | "allowance_insufficient"
113
+ | "native_balance_insufficient"
114
+ | "execution_reverted"
115
+ | "invalid_calldata"
116
+ | "rpc_unavailable"
117
+ | "unknown";
118
+
119
+ export type TxRouteStepKind = "swap" | "bridge" | "ibc_transfer" | "unknown";
120
+
121
+ export type TxRouteBridgeKind = "axelar" | "cctp" | "ibc" | "unknown";
122
+
123
+ export interface TxExecutionDiagnostics {
124
+ confirmation_failure_reason?: TxConfirmationFailureReason;
125
+ gas_estimate_context?: GasEstimateContext;
126
+ gas_estimate_error_category?: GasEstimateErrorCategory;
127
+ has_required_erc20_approval?: boolean;
128
+ has_native_value?: boolean;
129
+ failed_tx_has_required_erc20_approval?: boolean;
130
+ failed_tx_has_native_value?: boolean;
131
+ route_step_kinds?: TxRouteStepKind[];
132
+ route_bridge_kinds?: TxRouteBridgeKind[];
133
+ evm_simulation_outcome?: EvmGasSimulationOutcome;
134
+ }
135
+
97
136
  export interface TxExecutionBase {
98
137
  readonly id: string;
99
138
  status: TxExecutionStatus;
@@ -157,6 +196,7 @@ export interface PendingTxExecutionResult {
157
196
  status: BackgroundTxStatus;
158
197
  txHash?: string;
159
198
  error?: string;
199
+ diagnostics?: TxExecutionDiagnostics;
160
200
  }
161
201
 
162
202
  /**
@@ -166,4 +206,5 @@ export interface PendingTxExecutionResult {
166
206
  export interface TxExecutionResult {
167
207
  status: TxExecutionStatus;
168
208
  error?: string;
209
+ diagnostics?: TxExecutionDiagnostics;
169
210
  }
@@ -0,0 +1,89 @@
1
+ import {
2
+ classifyGasEstimateError,
3
+ createGasEstimateDiagnostics,
4
+ getTxExecutionDiagnostics,
5
+ hasEvmTxNativeValue,
6
+ sanitizeTxExecutionDiagnostics,
7
+ withTxExecutionDiagnostics,
8
+ } from "./diagnostics";
9
+
10
+ describe("tx execution diagnostics", () => {
11
+ it("classifies allowance failures without exposing the raw error", () => {
12
+ expect(
13
+ classifyGasEstimateError(
14
+ new Error(
15
+ "execution reverted: ERC20: transfer amount exceeds allowance"
16
+ )
17
+ )
18
+ ).toBe("allowance_insufficient");
19
+ });
20
+
21
+ it("classifies common gas estimate error families", () => {
22
+ expect(classifyGasEstimateError(new Error("insufficient funds"))).toBe(
23
+ "native_balance_insufficient"
24
+ );
25
+ expect(
26
+ classifyGasEstimateError(
27
+ new Error("execution reverted: panic: arithmetic underflow or overflow")
28
+ )
29
+ ).toBe("execution_reverted");
30
+ expect(
31
+ classifyGasEstimateError(
32
+ new Error("invalid argument 0: cannot unmarshal hex string")
33
+ )
34
+ ).toBe("invalid_calldata");
35
+ expect(classifyGasEstimateError(new Error("network error: timeout"))).toBe(
36
+ "rpc_unavailable"
37
+ );
38
+ });
39
+
40
+ it("detects native value on decimal and hex tx values", () => {
41
+ expect(hasEvmTxNativeValue("0")).toBe(false);
42
+ expect(hasEvmTxNativeValue("0x0")).toBe(false);
43
+ expect(hasEvmTxNativeValue("100")).toBe(true);
44
+ expect(hasEvmTxNativeValue("0x64")).toBe(true);
45
+ });
46
+
47
+ it("attaches sanitized gas estimate diagnostics to errors", () => {
48
+ const error = withTxExecutionDiagnostics(
49
+ new Error("execution reverted: ERC20: transfer amount exceeds allowance"),
50
+ createGasEstimateDiagnostics(
51
+ new Error(
52
+ "execution reverted: ERC20: transfer amount exceeds allowance"
53
+ ),
54
+ "background_fill_unsigned_tx",
55
+ {
56
+ value: "0x1",
57
+ requiredErc20Approvals: [{ tokenAddress: "0x1" }],
58
+ }
59
+ )
60
+ );
61
+
62
+ expect(error.message).toBe(
63
+ "execution reverted: ERC20: transfer amount exceeds allowance"
64
+ );
65
+ expect(getTxExecutionDiagnostics(error)).toEqual({
66
+ gas_estimate_context: "background_fill_unsigned_tx",
67
+ gas_estimate_error_category: "allowance_insufficient",
68
+ failed_tx_has_required_erc20_approval: true,
69
+ failed_tx_has_native_value: true,
70
+ });
71
+ });
72
+
73
+ it("drops unknown diagnostics keys and invalid enum values", () => {
74
+ expect(
75
+ sanitizeTxExecutionDiagnostics({
76
+ gas_estimate_context: "background_fill_unsigned_tx",
77
+ gas_estimate_error_category: "allowance_insufficient",
78
+ confirmation_failure_reason: "raw tx hash here",
79
+ route_step_kinds: ["swap", "raw-route-fingerprint"],
80
+ evm_simulation_outcome: "raw simulation outcome",
81
+ leaked_payload: "should not be logged",
82
+ })
83
+ ).toEqual({
84
+ gas_estimate_context: "background_fill_unsigned_tx",
85
+ gas_estimate_error_category: "allowance_insufficient",
86
+ route_step_kinds: ["swap"],
87
+ });
88
+ });
89
+ });
@@ -0,0 +1,298 @@
1
+ import { UnsignedTransaction } from "@ethersproject/transactions";
2
+ import { EvmGasSimulationOutcome } from "@keplr-wallet/types";
3
+ import {
4
+ GasEstimateContext,
5
+ GasEstimateErrorCategory,
6
+ TxConfirmationFailureReason,
7
+ TxExecutionDiagnostics,
8
+ TxRouteBridgeKind,
9
+ TxRouteStepKind,
10
+ } from "../types";
11
+
12
+ const CONFIRMATION_FAILURE_REASONS: readonly TxConfirmationFailureReason[] = [
13
+ "evm_receipt_missing",
14
+ "evm_receipt_status_failed",
15
+ "evm_trace_error",
16
+ "cosmos_trace_missing",
17
+ "cosmos_trace_error",
18
+ "cosmos_rest_fallback_missing",
19
+ "cosmos_code_nonzero",
20
+ ];
21
+
22
+ const GAS_ESTIMATE_CONTEXTS: readonly GasEstimateContext[] = [
23
+ "review_simulation",
24
+ "pre_sign_simulation",
25
+ "background_fill_unsigned_tx",
26
+ ];
27
+
28
+ const GAS_ESTIMATE_ERROR_CATEGORIES: readonly GasEstimateErrorCategory[] = [
29
+ "allowance_insufficient",
30
+ "native_balance_insufficient",
31
+ "execution_reverted",
32
+ "invalid_calldata",
33
+ "rpc_unavailable",
34
+ "unknown",
35
+ ];
36
+
37
+ const ROUTE_STEP_KINDS: readonly TxRouteStepKind[] = [
38
+ "swap",
39
+ "bridge",
40
+ "ibc_transfer",
41
+ "unknown",
42
+ ];
43
+
44
+ const ROUTE_BRIDGE_KINDS: readonly TxRouteBridgeKind[] = [
45
+ "axelar",
46
+ "cctp",
47
+ "ibc",
48
+ "unknown",
49
+ ];
50
+
51
+ const EVM_SIMULATION_OUTCOMES: readonly EvmGasSimulationOutcome[] = [
52
+ EvmGasSimulationOutcome.TX_SIMULATED,
53
+ EvmGasSimulationOutcome.TX_BUNDLE_SIMULATED,
54
+ EvmGasSimulationOutcome.APPROVAL_ONLY_SIMULATED,
55
+ ];
56
+
57
+ export class TxExecutionDiagnosticsError extends Error {
58
+ constructor(
59
+ message: string,
60
+ public readonly diagnostics: TxExecutionDiagnostics
61
+ ) {
62
+ super(message);
63
+ this.name = "TxExecutionDiagnosticsError";
64
+ }
65
+ }
66
+
67
+ export function getTxExecutionDiagnostics(
68
+ error: unknown
69
+ ): TxExecutionDiagnostics | undefined {
70
+ if (
71
+ error instanceof TxExecutionDiagnosticsError ||
72
+ (typeof error === "object" &&
73
+ error != null &&
74
+ "diagnostics" in error &&
75
+ typeof (error as { diagnostics?: unknown }).diagnostics === "object")
76
+ ) {
77
+ return sanitizeTxExecutionDiagnostics(
78
+ (error as { diagnostics: TxExecutionDiagnostics }).diagnostics
79
+ );
80
+ }
81
+ }
82
+
83
+ export function withTxExecutionDiagnostics(
84
+ error: unknown,
85
+ diagnostics: TxExecutionDiagnostics
86
+ ): TxExecutionDiagnosticsError {
87
+ const message =
88
+ typeof error === "object" &&
89
+ error != null &&
90
+ "message" in error &&
91
+ typeof (error as { message?: unknown }).message === "string"
92
+ ? (error as { message: string }).message
93
+ : String(error || "Unknown error");
94
+
95
+ return new TxExecutionDiagnosticsError(message, {
96
+ ...getTxExecutionDiagnostics(error),
97
+ ...sanitizeTxExecutionDiagnostics(diagnostics),
98
+ });
99
+ }
100
+
101
+ export function createGasEstimateDiagnostics(
102
+ error: unknown,
103
+ context: GasEstimateContext,
104
+ tx?: Pick<UnsignedTransaction, "value"> & {
105
+ requiredErc20Approvals?: unknown[];
106
+ }
107
+ ): TxExecutionDiagnostics {
108
+ return {
109
+ gas_estimate_context: context,
110
+ gas_estimate_error_category: classifyGasEstimateError(error),
111
+ ...(tx
112
+ ? {
113
+ failed_tx_has_required_erc20_approval:
114
+ (tx.requiredErc20Approvals?.length ?? 0) > 0,
115
+ failed_tx_has_native_value: hasEvmTxNativeValue(tx.value),
116
+ }
117
+ : {}),
118
+ };
119
+ }
120
+
121
+ export function classifyGasEstimateError(
122
+ error: unknown
123
+ ): GasEstimateErrorCategory {
124
+ const message = getErrorMessage(error).toLowerCase();
125
+
126
+ if (message.includes("allowance")) {
127
+ return "allowance_insufficient";
128
+ }
129
+
130
+ if (
131
+ matchesAny(message, [
132
+ "insufficient funds",
133
+ "insufficient balance",
134
+ "insufficient native",
135
+ "exceeds balance",
136
+ "not enough balance",
137
+ "not enough gas",
138
+ ])
139
+ ) {
140
+ return "native_balance_insufficient";
141
+ }
142
+
143
+ if (
144
+ matchesAny(message, [
145
+ "invalid argument",
146
+ "invalid calldata",
147
+ "invalid data",
148
+ "invalid hex",
149
+ "cannot unmarshal",
150
+ "missing value for required argument",
151
+ ])
152
+ ) {
153
+ return "invalid_calldata";
154
+ }
155
+
156
+ if (message.includes("execution reverted") || message.includes("revert")) {
157
+ return "execution_reverted";
158
+ }
159
+
160
+ if (
161
+ matchesAny(message, [
162
+ "failed to fetch",
163
+ "network error",
164
+ "timeout",
165
+ "timed out",
166
+ "too many requests",
167
+ "bad gateway",
168
+ "service unavailable",
169
+ "gateway timeout",
170
+ ])
171
+ ) {
172
+ return "rpc_unavailable";
173
+ }
174
+
175
+ return "unknown";
176
+ }
177
+
178
+ export function hasEvmTxNativeValue(value: unknown): boolean {
179
+ if (value == null) {
180
+ return false;
181
+ }
182
+
183
+ try {
184
+ return BigInt(value.toString()) > BigInt(0);
185
+ } catch {
186
+ return false;
187
+ }
188
+ }
189
+
190
+ export function sanitizeTxExecutionDiagnostics(
191
+ diagnostics: unknown
192
+ ): TxExecutionDiagnostics | undefined {
193
+ if (!diagnostics || typeof diagnostics !== "object") {
194
+ return undefined;
195
+ }
196
+
197
+ const source = diagnostics as Record<string, unknown>;
198
+ const sanitized: TxExecutionDiagnostics = {};
199
+ const confirmationFailureReason = source["confirmation_failure_reason"];
200
+ const gasEstimateContext = source["gas_estimate_context"];
201
+ const gasEstimateErrorCategory = source["gas_estimate_error_category"];
202
+ const hasRequiredErc20Approval = source["has_required_erc20_approval"];
203
+ const hasNativeValue = source["has_native_value"];
204
+ const failedTxHasRequiredErc20Approval =
205
+ source["failed_tx_has_required_erc20_approval"];
206
+ const failedTxHasNativeValue = source["failed_tx_has_native_value"];
207
+ const routeStepKindsValue = source["route_step_kinds"];
208
+ const routeBridgeKindsValue = source["route_bridge_kinds"];
209
+ const evmSimulationOutcome = source["evm_simulation_outcome"];
210
+
211
+ if (isOneOf(confirmationFailureReason, CONFIRMATION_FAILURE_REASONS)) {
212
+ sanitized.confirmation_failure_reason = confirmationFailureReason;
213
+ }
214
+
215
+ if (isOneOf(gasEstimateContext, GAS_ESTIMATE_CONTEXTS)) {
216
+ sanitized.gas_estimate_context = gasEstimateContext;
217
+ }
218
+
219
+ if (isOneOf(gasEstimateErrorCategory, GAS_ESTIMATE_ERROR_CATEGORIES)) {
220
+ sanitized.gas_estimate_error_category = gasEstimateErrorCategory;
221
+ }
222
+
223
+ if (typeof hasRequiredErc20Approval === "boolean") {
224
+ sanitized.has_required_erc20_approval = hasRequiredErc20Approval;
225
+ }
226
+
227
+ if (typeof hasNativeValue === "boolean") {
228
+ sanitized.has_native_value = hasNativeValue;
229
+ }
230
+
231
+ if (typeof failedTxHasRequiredErc20Approval === "boolean") {
232
+ sanitized.failed_tx_has_required_erc20_approval =
233
+ failedTxHasRequiredErc20Approval;
234
+ }
235
+
236
+ if (typeof failedTxHasNativeValue === "boolean") {
237
+ sanitized.failed_tx_has_native_value = failedTxHasNativeValue;
238
+ }
239
+
240
+ const routeStepKinds = sanitizeStringArray(routeStepKindsValue, [
241
+ ...ROUTE_STEP_KINDS,
242
+ ]);
243
+ if (routeStepKinds) {
244
+ sanitized.route_step_kinds = routeStepKinds;
245
+ }
246
+
247
+ const routeBridgeKinds = sanitizeStringArray(routeBridgeKindsValue, [
248
+ ...ROUTE_BRIDGE_KINDS,
249
+ ]);
250
+ if (routeBridgeKinds) {
251
+ sanitized.route_bridge_kinds = routeBridgeKinds;
252
+ }
253
+
254
+ if (isOneOf(evmSimulationOutcome, EVM_SIMULATION_OUTCOMES)) {
255
+ sanitized.evm_simulation_outcome = evmSimulationOutcome;
256
+ }
257
+
258
+ return Object.keys(sanitized).length > 0 ? sanitized : undefined;
259
+ }
260
+
261
+ function getErrorMessage(error: unknown): string {
262
+ if (
263
+ typeof error === "object" &&
264
+ error != null &&
265
+ "message" in error &&
266
+ typeof (error as { message?: unknown }).message === "string"
267
+ ) {
268
+ return (error as { message: string }).message;
269
+ }
270
+
271
+ return String(error || "");
272
+ }
273
+
274
+ function matchesAny(message: string, patterns: string[]): boolean {
275
+ return patterns.some((pattern) => message.includes(pattern));
276
+ }
277
+
278
+ function isOneOf<T extends string>(
279
+ value: unknown,
280
+ allowedValues: readonly T[]
281
+ ): value is T {
282
+ return typeof value === "string" && allowedValues.includes(value as T);
283
+ }
284
+
285
+ function sanitizeStringArray<T extends string>(
286
+ value: unknown,
287
+ allowedValues: readonly T[]
288
+ ): T[] | undefined {
289
+ if (!Array.isArray(value)) {
290
+ return undefined;
291
+ }
292
+
293
+ const sanitized = value.filter((item): item is T =>
294
+ isOneOf(item, allowedValues)
295
+ );
296
+
297
+ return sanitized.length > 0 ? [...new Set(sanitized)] : undefined;
298
+ }