@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.
- package/build/keyring/service.js +31 -42
- package/build/keyring/service.js.map +1 -1
- package/build/keyring/service.spec.d.ts +1 -0
- package/build/keyring/service.spec.js +106 -0
- package/build/keyring/service.spec.js.map +1 -0
- package/build/tx-executor/index.d.ts +1 -0
- package/build/tx-executor/index.js +1 -0
- package/build/tx-executor/index.js.map +1 -1
- package/build/tx-executor/service.d.ts +8 -5
- package/build/tx-executor/service.js +56 -11
- package/build/tx-executor/service.js.map +1 -1
- package/build/tx-executor/types.d.ts +20 -1
- package/build/tx-executor/utils/diagnostics.d.ts +14 -0
- package/build/tx-executor/utils/diagnostics.js +215 -0
- package/build/tx-executor/utils/diagnostics.js.map +1 -0
- package/build/tx-executor/utils/diagnostics.spec.d.ts +1 -0
- package/build/tx-executor/utils/diagnostics.spec.js +48 -0
- package/build/tx-executor/utils/diagnostics.spec.js.map +1 -0
- package/build/tx-executor/utils/evm.js +7 -3
- package/build/tx-executor/utils/evm.js.map +1 -1
- package/build/tx-executor/utils/evm.spec.d.ts +1 -0
- package/build/tx-executor/utils/evm.spec.js +59 -0
- package/build/tx-executor/utils/evm.spec.js.map +1 -0
- package/package.json +13 -13
- package/src/keyring/service.spec.ts +141 -0
- package/src/keyring/service.ts +47 -59
- package/src/tx-executor/index.ts +1 -0
- package/src/tx-executor/service.ts +72 -17
- package/src/tx-executor/types.ts +42 -1
- package/src/tx-executor/utils/diagnostics.spec.ts +89 -0
- package/src/tx-executor/utils/diagnostics.ts +298 -0
- package/src/tx-executor/utils/evm.spec.ts +62 -0
- 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 {
|
|
53
|
-
|
|
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
|
|
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<
|
|
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<
|
|
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
|
|
869
|
+
return {
|
|
870
|
+
confirmed: false,
|
|
871
|
+
diagnostics: {
|
|
872
|
+
confirmation_failure_reason: "evm_receipt_missing",
|
|
873
|
+
},
|
|
874
|
+
};
|
|
855
875
|
}
|
|
856
876
|
|
|
857
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
|
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
|
|
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 }
|
|
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
|
|
957
|
+
return { failed: true };
|
|
903
958
|
}
|
|
904
959
|
}
|
|
905
960
|
|
package/src/tx-executor/types.ts
CHANGED
|
@@ -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
|
+
}
|