@persistenceone/bridgekitty 0.3.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/dist/backends/across.d.ts +10 -0
  4. package/dist/backends/across.js +285 -0
  5. package/dist/backends/debridge.d.ts +11 -0
  6. package/dist/backends/debridge.js +380 -0
  7. package/dist/backends/lifi.d.ts +19 -0
  8. package/dist/backends/lifi.js +295 -0
  9. package/dist/backends/persistence.d.ts +86 -0
  10. package/dist/backends/persistence.js +642 -0
  11. package/dist/backends/relay.d.ts +11 -0
  12. package/dist/backends/relay.js +292 -0
  13. package/dist/backends/squid.d.ts +31 -0
  14. package/dist/backends/squid.js +476 -0
  15. package/dist/backends/types.d.ts +125 -0
  16. package/dist/backends/types.js +11 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +154 -0
  19. package/dist/routing/engine.d.ts +49 -0
  20. package/dist/routing/engine.js +336 -0
  21. package/dist/tools/check-status.d.ts +3 -0
  22. package/dist/tools/check-status.js +93 -0
  23. package/dist/tools/execute-bridge.d.ts +3 -0
  24. package/dist/tools/execute-bridge.js +428 -0
  25. package/dist/tools/get-chains.d.ts +3 -0
  26. package/dist/tools/get-chains.js +162 -0
  27. package/dist/tools/get-quote.d.ts +3 -0
  28. package/dist/tools/get-quote.js +534 -0
  29. package/dist/tools/get-tokens.d.ts +3 -0
  30. package/dist/tools/get-tokens.js +128 -0
  31. package/dist/tools/help.d.ts +2 -0
  32. package/dist/tools/help.js +204 -0
  33. package/dist/tools/multi-quote.d.ts +3 -0
  34. package/dist/tools/multi-quote.js +310 -0
  35. package/dist/tools/onboard.d.ts +3 -0
  36. package/dist/tools/onboard.js +218 -0
  37. package/dist/tools/wallet.d.ts +14 -0
  38. package/dist/tools/wallet.js +744 -0
  39. package/dist/tools/xprt-farm.d.ts +3 -0
  40. package/dist/tools/xprt-farm.js +1308 -0
  41. package/dist/tools/xprt-rewards.d.ts +2 -0
  42. package/dist/tools/xprt-rewards.js +177 -0
  43. package/dist/tools/xprt-staking.d.ts +2 -0
  44. package/dist/tools/xprt-staking.js +565 -0
  45. package/dist/utils/chains.d.ts +22 -0
  46. package/dist/utils/chains.js +154 -0
  47. package/dist/utils/circuit-breaker.d.ts +64 -0
  48. package/dist/utils/circuit-breaker.js +160 -0
  49. package/dist/utils/evm.d.ts +18 -0
  50. package/dist/utils/evm.js +46 -0
  51. package/dist/utils/fill-detector.d.ts +70 -0
  52. package/dist/utils/fill-detector.js +298 -0
  53. package/dist/utils/gas-estimator.d.ts +67 -0
  54. package/dist/utils/gas-estimator.js +340 -0
  55. package/dist/utils/sanitize-error.d.ts +23 -0
  56. package/dist/utils/sanitize-error.js +101 -0
  57. package/dist/utils/token-registry.d.ts +70 -0
  58. package/dist/utils/token-registry.js +669 -0
  59. package/dist/utils/tokens.d.ts +17 -0
  60. package/dist/utils/tokens.js +37 -0
  61. package/dist/utils/tx-simulator.d.ts +27 -0
  62. package/dist/utils/tx-simulator.js +105 -0
  63. package/package.json +75 -0
@@ -0,0 +1,154 @@
1
+ // Synthetic chain IDs for non-EVM chains.
2
+ // These are high positive integers that won't collide with real EVM chain IDs
3
+ // but satisfy the routing engine's requirement for positive integer chain IDs.
4
+ export const PERSISTENCE_CHAIN_ID = 9999001;
5
+ export const COSMOSHUB_CHAIN_ID = 9999002;
6
+ // Solana uses its real chain ID (as recognized by deBridge)
7
+ export const SOLANA_CHAIN_ID = 7565164;
8
+ const CHAINS = [
9
+ { id: 1, name: "Ethereum", key: "ethereum" },
10
+ { id: 10, name: "Optimism", key: "optimism" },
11
+ { id: 25, name: "Cronos", key: "cronos" },
12
+ { id: 56, name: "BNB Chain", key: "bsc" },
13
+ { id: 100, name: "Gnosis", key: "gnosis" },
14
+ { id: 122, name: "Fuse", key: "fuse" },
15
+ { id: 130, name: "Unichain", key: "unichain" },
16
+ { id: 137, name: "Polygon", key: "polygon" },
17
+ { id: 143, name: "Monad", key: "monad" },
18
+ { id: 196, name: "X Layer", key: "xlayer" },
19
+ { id: 250, name: "Fantom", key: "fantom" },
20
+ { id: 252, name: "Fraxtal", key: "fraxtal" },
21
+ { id: 288, name: "Boba", key: "boba" },
22
+ { id: 324, name: "zkSync Era", key: "zksync" },
23
+ { id: 480, name: "World Chain", key: "world-chain" },
24
+ { id: 690, name: "Redstone", key: "redstone" },
25
+ { id: 1088, name: "Metis", key: "metis" },
26
+ { id: 1101, name: "Polygon zkEVM", key: "polygon-zkevm" },
27
+ { id: 1135, name: "Lisk", key: "lisk" },
28
+ { id: 1284, name: "Moonbeam", key: "moonbeam" },
29
+ { id: 1329, name: "Sei", key: "sei" },
30
+ { id: 1625, name: "Gravity", key: "gravity" },
31
+ { id: 1868, name: "Soneium", key: "soneium" },
32
+ { id: 1923, name: "Swellchain", key: "swellchain" },
33
+ { id: 2741, name: "Abstract", key: "abstract" },
34
+ { id: 5000, name: "Mantle", key: "mantle" },
35
+ { id: 7560, name: "Cyber", key: "cyber" },
36
+ { id: 7777777, name: "Zora", key: "zora" },
37
+ { id: 8453, name: "Base", key: "base" },
38
+ { id: 13371, name: "Immutable zkEVM", key: "immutable-zkevm" },
39
+ { id: 17000, name: "Holesky", key: "holesky" },
40
+ { id: 30, name: "Rootstock", key: "rootstock" },
41
+ { id: 33139, name: "ApeChain", key: "apechain" },
42
+ { id: 34443, name: "Mode", key: "mode" },
43
+ { id: 42161, name: "Arbitrum", key: "arbitrum" },
44
+ { id: 42170, name: "Arbitrum Nova", key: "arbitrum-nova" },
45
+ { id: 42220, name: "Celo", key: "celo" },
46
+ { id: 43114, name: "Avalanche", key: "avalanche" },
47
+ { id: 44787, name: "Celo Alfajores", key: "celo-alfajores" },
48
+ { id: 57073, name: "Ink", key: "ink" },
49
+ { id: 59144, name: "Linea", key: "linea" },
50
+ { id: 60808, name: "Bob", key: "bob" },
51
+ { id: 81457, name: "Blast", key: "blast" },
52
+ { id: 146, name: "Sonic", key: "sonic" },
53
+ { id: 167000, name: "Taiko", key: "taiko" },
54
+ { id: 2020, name: "Ronin", key: "ronin" },
55
+ { id: 204, name: "opBNB", key: "opbnb" },
56
+ { id: 534352, name: "Scroll", key: "scroll" },
57
+ { id: 7225878, name: "Saakuru", key: "saakuru" },
58
+ { id: 666666666, name: "Degen", key: "degen" },
59
+ { id: 80094, name: "Berachain", key: "berachain" },
60
+ { id: 50104, name: "Sophon", key: "sophon" },
61
+ { id: 37714555429, name: "Xai", key: "xai" },
62
+ { id: 14, name: "Flare", key: "flare" },
63
+ { id: 1516, name: "Story", key: "story" },
64
+ { id: 4801, name: "World Chain Testnet", key: "world-chain-testnet" },
65
+ { id: 8217, name: "Kaia", key: "kaia" },
66
+ { id: 1750, name: "Metal L2", key: "metal" },
67
+ { id: 2522, name: "Shadow", key: "shadow" },
68
+ { id: 98865, name: "Plume", key: "plume" },
69
+ { id: 21000000, name: "Corn", key: "corn" },
70
+ { id: 232, name: "Lens", key: "lens" },
71
+ { id: 999, name: "HyperEVM", key: "hyperevm" },
72
+ { id: 360, name: "Shape", key: "shape" },
73
+ { id: 1514, name: "Hemi", key: "hemi" },
74
+ { id: 810180, name: "zkLink Nova", key: "zklink-nova" },
75
+ { id: 4326, name: "MegaETH", key: "megaeth" },
76
+ { id: 9745, name: "Plasma", key: "plasma" },
77
+ // Cosmos chains (synthetic IDs — mapped to real chain ID strings by Squid backend)
78
+ { id: PERSISTENCE_CHAIN_ID, name: "Persistence", key: "persistence" },
79
+ { id: COSMOSHUB_CHAIN_ID, name: "Cosmos Hub", key: "cosmoshub" },
80
+ // Solana (uses real chain ID as recognized by deBridge)
81
+ { id: SOLANA_CHAIN_ID, name: "Solana", key: "solana" },
82
+ ];
83
+ // Backend-specific chain ID overrides (reserved for future non-EVM chain support)
84
+ const CHAIN_ID_OVERRIDES = {};
85
+ export function getBackendChainId(backendName, chainId) {
86
+ const key = backendName.toLowerCase().replace(/\s*\(.*\)/, "");
87
+ return CHAIN_ID_OVERRIDES[key]?.[chainId] ?? chainId;
88
+ }
89
+ // Mapping from chain key → real Cosmos chain ID string (for Squid Router / IBC)
90
+ export const COSMOS_CHAIN_IDS = {
91
+ persistence: "core-1",
92
+ cosmoshub: "cosmoshub-4",
93
+ };
94
+ // Reverse mapping: synthetic numeric ID → Cosmos chain ID string (as used by Squid Router)
95
+ export const SYNTHETIC_TO_COSMOS = {
96
+ [PERSISTENCE_CHAIN_ID]: "core-1",
97
+ [COSMOSHUB_CHAIN_ID]: "cosmoshub-4",
98
+ };
99
+ export function resolveChainId(input) {
100
+ // Try numeric: only accept chain IDs that are in the known CHAINS array (V3-LOW-003)
101
+ const num = Number(input);
102
+ if (!isNaN(num) && Number.isInteger(num) && num > 0) {
103
+ const known = CHAINS.find((c) => c.id === num);
104
+ return known ? known.id : null;
105
+ }
106
+ // Also accept Cosmos chain ID strings directly (e.g. "core-1", "cosmoshub-4")
107
+ const lower = input.toLowerCase().trim();
108
+ for (const [syntheticId, cosmosId] of Object.entries(SYNTHETIC_TO_COSMOS)) {
109
+ if (lower === cosmosId || lower === cosmosId.replace(/-/g, "")) {
110
+ return Number(syntheticId);
111
+ }
112
+ }
113
+ // Also accept common aliases for Cosmos chains
114
+ if (lower === "persistence-core-1" || lower === "persistencecore1") {
115
+ return PERSISTENCE_CHAIN_ID;
116
+ }
117
+ if (lower === "cosmoshub4") {
118
+ return COSMOSHUB_CHAIN_ID;
119
+ }
120
+ // Solana aliases
121
+ if (lower === "sol" || lower === "solana") {
122
+ return SOLANA_CHAIN_ID;
123
+ }
124
+ const match = CHAINS.find((c) => c.key === lower || c.name.toLowerCase() === lower);
125
+ return match?.id ?? null;
126
+ }
127
+ /** Check if a chain key or ID refers to Solana */
128
+ export function isSolanaChain(chainKeyOrId) {
129
+ if (typeof chainKeyOrId === "number") {
130
+ return chainKeyOrId === SOLANA_CHAIN_ID;
131
+ }
132
+ return chainKeyOrId.toLowerCase() === "solana" || chainKeyOrId.toLowerCase() === "sol";
133
+ }
134
+ /** Check if a chain key or ID refers to a Cosmos chain */
135
+ export function isCosmosChain(chainKeyOrId) {
136
+ if (typeof chainKeyOrId === "number") {
137
+ return SYNTHETIC_TO_COSMOS[chainKeyOrId] !== undefined;
138
+ }
139
+ return COSMOS_CHAIN_IDS[chainKeyOrId.toLowerCase()] !== undefined;
140
+ }
141
+ /** Get the Cosmos chain ID string for Squid Router from a synthetic numeric ID */
142
+ export function getCosmosChainIdFromSynthetic(syntheticId) {
143
+ return SYNTHETIC_TO_COSMOS[syntheticId] ?? null;
144
+ }
145
+ /** Get the Cosmos chain ID string from a chain key */
146
+ export function getCosmosChainId(key) {
147
+ return COSMOS_CHAIN_IDS[key.toLowerCase()] ?? null;
148
+ }
149
+ export function getChainName(id) {
150
+ return CHAINS.find((c) => c.id === id)?.name ?? `Chain ${id}`;
151
+ }
152
+ export function getAllChains() {
153
+ return CHAINS;
154
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Circuit breaker for backend failure management.
3
+ *
4
+ * States:
5
+ * - CLOSED: Normal operation, requests pass through
6
+ * - OPEN: Backend is failing, skip requests for a cooldown period
7
+ * - HALF_OPEN: Testing with a single request after cooldown
8
+ *
9
+ * Transitions:
10
+ * - CLOSED → OPEN: After `failureThreshold` failures within `failureWindowMs`
11
+ * - OPEN → HALF_OPEN: After `cooldownMs` elapsed
12
+ * - HALF_OPEN → CLOSED: On success
13
+ * - HALF_OPEN → OPEN: On failure (with extended cooldown)
14
+ */
15
+ export type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
16
+ interface CircuitBreakerConfig {
17
+ /** Number of failures before opening the circuit (default: 3) */
18
+ failureThreshold?: number;
19
+ /** Time window for counting failures in ms (default: 300_000 = 5 min) */
20
+ failureWindowMs?: number;
21
+ /** How long to wait before testing again in ms (default: 30_000 = 30s) */
22
+ cooldownMs?: number;
23
+ /** Extended cooldown after HALF_OPEN failure in ms (default: 60_000 = 60s) */
24
+ extendedCooldownMs?: number;
25
+ }
26
+ export declare class CircuitBreaker {
27
+ private circuits;
28
+ private halfOpenLocks;
29
+ private config;
30
+ constructor(config?: CircuitBreakerConfig);
31
+ /**
32
+ * Check if a backend is allowed to receive requests.
33
+ * Returns true if the backend should be called, false if it should be skipped.
34
+ */
35
+ isAllowed(backendName: string): boolean;
36
+ /**
37
+ * Report a successful request for a backend.
38
+ * Gradual recovery: removes the oldest failure instead of clearing all at once.
39
+ * Full reset only happens from HALF_OPEN state (successful probe).
40
+ */
41
+ recordSuccess(backendName: string): void;
42
+ /**
43
+ * Report a failed request for a backend.
44
+ * May transition to OPEN if threshold is exceeded.
45
+ */
46
+ recordFailure(backendName: string): void;
47
+ /**
48
+ * Get the current state of a backend's circuit.
49
+ */
50
+ getState(backendName: string): CircuitState;
51
+ /**
52
+ * Get a summary of all circuit states (for debugging/monitoring).
53
+ */
54
+ getAll(): Record<string, CircuitState>;
55
+ /**
56
+ * Reset a specific backend's circuit (for testing/admin).
57
+ */
58
+ reset(backendName: string): void;
59
+ /**
60
+ * Reset all circuits.
61
+ */
62
+ resetAll(): void;
63
+ }
64
+ export {};
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Circuit breaker for backend failure management.
3
+ *
4
+ * States:
5
+ * - CLOSED: Normal operation, requests pass through
6
+ * - OPEN: Backend is failing, skip requests for a cooldown period
7
+ * - HALF_OPEN: Testing with a single request after cooldown
8
+ *
9
+ * Transitions:
10
+ * - CLOSED → OPEN: After `failureThreshold` failures within `failureWindowMs`
11
+ * - OPEN → HALF_OPEN: After `cooldownMs` elapsed
12
+ * - HALF_OPEN → CLOSED: On success
13
+ * - HALF_OPEN → OPEN: On failure (with extended cooldown)
14
+ */
15
+ const DEFAULT_CONFIG = {
16
+ failureThreshold: 3,
17
+ failureWindowMs: 300_000,
18
+ cooldownMs: 30_000,
19
+ extendedCooldownMs: 60_000,
20
+ };
21
+ export class CircuitBreaker {
22
+ circuits = new Map();
23
+ halfOpenLocks = new Set();
24
+ config;
25
+ constructor(config) {
26
+ this.config = { ...DEFAULT_CONFIG, ...config };
27
+ }
28
+ /**
29
+ * Check if a backend is allowed to receive requests.
30
+ * Returns true if the backend should be called, false if it should be skipped.
31
+ */
32
+ isAllowed(backendName) {
33
+ const circuit = this.circuits.get(backendName);
34
+ if (!circuit)
35
+ return true; // No circuit = never failed = allowed
36
+ const now = Date.now();
37
+ switch (circuit.state) {
38
+ case "CLOSED":
39
+ return true;
40
+ case "OPEN": {
41
+ // Check if cooldown has elapsed → transition to HALF_OPEN
42
+ if (now - circuit.openedAt >= circuit.cooldownMs) {
43
+ circuit.state = "HALF_OPEN";
44
+ // Lock so only the first caller gets through (V3-LOW-001)
45
+ if (this.halfOpenLocks.has(backendName))
46
+ return false;
47
+ this.halfOpenLocks.add(backendName);
48
+ return true; // Allow one test request
49
+ }
50
+ return false; // Still in cooldown
51
+ }
52
+ case "HALF_OPEN":
53
+ // Only one concurrent request allowed in HALF_OPEN (V3-LOW-001)
54
+ if (this.halfOpenLocks.has(backendName))
55
+ return false;
56
+ this.halfOpenLocks.add(backendName);
57
+ return true;
58
+ default:
59
+ return true;
60
+ }
61
+ }
62
+ /**
63
+ * Report a successful request for a backend.
64
+ * Gradual recovery: removes the oldest failure instead of clearing all at once.
65
+ * Full reset only happens from HALF_OPEN state (successful probe).
66
+ */
67
+ recordSuccess(backendName) {
68
+ const circuit = this.circuits.get(backendName);
69
+ if (!circuit)
70
+ return;
71
+ if (circuit.state === "HALF_OPEN") {
72
+ // Successful probe — fully reset
73
+ this.halfOpenLocks.delete(backendName);
74
+ console.error(`[circuit-breaker] ${backendName}: HALF_OPEN → CLOSED (probe succeeded)`);
75
+ circuit.state = "CLOSED";
76
+ circuit.failures = [];
77
+ circuit.cooldownMs = this.config.cooldownMs;
78
+ }
79
+ else if (circuit.state === "CLOSED" && circuit.failures.length > 0) {
80
+ // Gradual recovery: remove oldest failure on each success
81
+ circuit.failures.shift();
82
+ }
83
+ }
84
+ /**
85
+ * Report a failed request for a backend.
86
+ * May transition to OPEN if threshold is exceeded.
87
+ */
88
+ recordFailure(backendName) {
89
+ const now = Date.now();
90
+ let circuit = this.circuits.get(backendName);
91
+ if (!circuit) {
92
+ circuit = {
93
+ state: "CLOSED",
94
+ failures: [],
95
+ openedAt: 0,
96
+ cooldownMs: this.config.cooldownMs,
97
+ };
98
+ this.circuits.set(backendName, circuit);
99
+ }
100
+ if (circuit.state === "HALF_OPEN") {
101
+ // Test request failed → back to OPEN with extended cooldown
102
+ this.halfOpenLocks.delete(backendName);
103
+ circuit.state = "OPEN";
104
+ circuit.openedAt = now;
105
+ circuit.cooldownMs = this.config.extendedCooldownMs;
106
+ console.error(`[circuit-breaker] ${backendName}: HALF_OPEN → OPEN (probe failed, extended cooldown ${this.config.extendedCooldownMs}ms)`);
107
+ return;
108
+ }
109
+ // Add failure timestamp (prune old failures outside the window)
110
+ circuit.failures = circuit.failures
111
+ .filter((t) => now - t < this.config.failureWindowMs)
112
+ .concat(now);
113
+ // Check if threshold exceeded
114
+ if (circuit.failures.length >= this.config.failureThreshold) {
115
+ circuit.state = "OPEN";
116
+ circuit.openedAt = now;
117
+ circuit.cooldownMs = this.config.cooldownMs;
118
+ console.error(`[circuit-breaker] ${backendName}: CLOSED → OPEN (${circuit.failures.length} failures in ${this.config.failureWindowMs}ms window, cooldown ${this.config.cooldownMs}ms)`);
119
+ }
120
+ }
121
+ /**
122
+ * Get the current state of a backend's circuit.
123
+ */
124
+ getState(backendName) {
125
+ const circuit = this.circuits.get(backendName);
126
+ if (!circuit)
127
+ return "CLOSED";
128
+ // Check for OPEN → HALF_OPEN transition
129
+ if (circuit.state === "OPEN") {
130
+ if (Date.now() - circuit.openedAt >= circuit.cooldownMs) {
131
+ circuit.state = "HALF_OPEN";
132
+ }
133
+ }
134
+ return circuit.state;
135
+ }
136
+ /**
137
+ * Get a summary of all circuit states (for debugging/monitoring).
138
+ */
139
+ getAll() {
140
+ const result = {};
141
+ for (const [name] of this.circuits) {
142
+ result[name] = this.getState(name);
143
+ }
144
+ return result;
145
+ }
146
+ /**
147
+ * Reset a specific backend's circuit (for testing/admin).
148
+ */
149
+ reset(backendName) {
150
+ this.circuits.delete(backendName);
151
+ this.halfOpenLocks.delete(backendName);
152
+ }
153
+ /**
154
+ * Reset all circuits.
155
+ */
156
+ resetAll() {
157
+ this.circuits.clear();
158
+ this.halfOpenLocks.clear();
159
+ }
160
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared EVM utilities used across multiple backends.
3
+ */
4
+ /**
5
+ * Build ERC20 approve(address spender, uint256 amount) calldata.
6
+ */
7
+ export declare function buildApproveData(spender: string, amount: string): string;
8
+ /**
9
+ * Check if an address is the native token (zero address).
10
+ */
11
+ export declare const NATIVE_ADDRESS = "0x0000000000000000000000000000000000000000";
12
+ export declare function isNativeToken(address: string): boolean;
13
+ /**
14
+ * Validate an EVM hex address (0x + 40 hex chars).
15
+ * Returns { valid, warning? } — accepts all valid hex addresses but warns
16
+ * on mixed-case addresses that fail EIP-55 checksum validation (MEDIUM-002).
17
+ */
18
+ export declare function isValidEvmAddress(address: string): boolean;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared EVM utilities used across multiple backends.
3
+ */
4
+ import { getAddress } from "ethers";
5
+ /**
6
+ * Build ERC20 approve(address spender, uint256 amount) calldata.
7
+ */
8
+ export function buildApproveData(spender, amount) {
9
+ // ERC20 approve(address,uint256) selector = 0x095ea7b3
10
+ const spenderPadded = spender.toLowerCase().replace("0x", "").padStart(64, "0");
11
+ const amountHex = BigInt(amount).toString(16).padStart(64, "0");
12
+ return `0x095ea7b3${spenderPadded}${amountHex}`;
13
+ }
14
+ /**
15
+ * Check if an address is the native token (zero address).
16
+ */
17
+ export const NATIVE_ADDRESS = "0x0000000000000000000000000000000000000000";
18
+ export function isNativeToken(address) {
19
+ return (address === NATIVE_ADDRESS ||
20
+ address === "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" ||
21
+ address.toLowerCase() === "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee");
22
+ }
23
+ /**
24
+ * Validate an EVM hex address (0x + 40 hex chars).
25
+ * Returns { valid, warning? } — accepts all valid hex addresses but warns
26
+ * on mixed-case addresses that fail EIP-55 checksum validation (MEDIUM-002).
27
+ */
28
+ export function isValidEvmAddress(address) {
29
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address))
30
+ return false;
31
+ // If all-lowercase or all-uppercase (after 0x), no checksum to validate
32
+ const body = address.slice(2);
33
+ if (body === body.toLowerCase() || body === body.toUpperCase())
34
+ return true;
35
+ // Mixed-case: validate EIP-55 checksum using ethers
36
+ try {
37
+ const checksummed = getAddress(address.toLowerCase());
38
+ if (checksummed !== address) {
39
+ console.warn(`[evm] MEDIUM-002: Address ${address} has invalid mixed-case (expected EIP-55: ${checksummed}). Accepting but flagging.`);
40
+ }
41
+ }
42
+ catch {
43
+ console.warn(`[evm] MEDIUM-002: Could not verify EIP-55 checksum for ${address}. Accepting anyway.`);
44
+ }
45
+ return true;
46
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Multi-signal fill detector for cross-chain bridge fills.
3
+ *
4
+ * Public RPCs aggressively cache ALL RPC responses (eth_call, eth_getLogs)
5
+ * for 30-120s server-side. WebSocket transport does NOT bypass this.
6
+ *
7
+ * This module provides five detection methods:
8
+ *
9
+ * 1. **eth_subscribe("logs")** — push-based real-time subscription via raw
10
+ * WebSocket. The server pushes matching logs as new blocks are mined.
11
+ * NOT cached because each block is novel. Detects in 3-15s.
12
+ * Uses BROAD filter (token + Transfer topic only) with client-side
13
+ * recipient filtering, because some RPCs don't support topic[2] filtering
14
+ * in subscriptions. ONLY uses drpc (publicnode silently drops log events).
15
+ *
16
+ * 2. **newHeads + targeted getLogs** — subscribe to new block headers via WS,
17
+ * then do a getLogs for just that block number. Bypasses RPC caching
18
+ * because we query a specific (newly mined) block.
19
+ *
20
+ * 3. HTTP getLogs — rotated across RPCs, 30-120s (server-side cached)
21
+ * 4. HTTP balance check — rotated across RPCs, 30-120s (server-side cached)
22
+ * 5. Status API — Persistence backend (often broken)
23
+ *
24
+ * IMPORTANT: Create the FillWatcher BEFORE the initiate transaction to
25
+ * allow time for WebSocket connection + subscription establishment.
26
+ */
27
+ export interface FillWatcher {
28
+ /** Non-blocking check: has the fill been detected? */
29
+ isDetected: () => boolean;
30
+ /** How many WS connections are alive? */
31
+ connectedCount: () => number;
32
+ /** Clean up all WebSocket connections. Must be called when done. */
33
+ cleanup: () => void;
34
+ }
35
+ /**
36
+ * Create a push-based fill watcher using raw WebSocket eth_subscribe.
37
+ *
38
+ * Two strategies run in parallel:
39
+ * A) eth_subscribe("logs") on drpc — direct Transfer event push (fastest)
40
+ * B) eth_subscribe("newHeads") — on each new block, do a targeted
41
+ * getLogs(blockNum, blockNum) which bypasses RPC caching
42
+ *
43
+ * Call BEFORE signAndExecute to give connections time to establish (~2-5s).
44
+ * Check isDetected() in the polling loop — returns true as soon as a
45
+ * matching Transfer event is found by ANY method.
46
+ */
47
+ export declare function createFillWatcher(chainId: number, tokenAddress: string, walletAddress: string, onDetected?: () => void): FillWatcher;
48
+ export interface TransferEventResult {
49
+ found: boolean;
50
+ latestBlock?: number;
51
+ }
52
+ export interface BalanceChangeResult {
53
+ changed: boolean;
54
+ newBalance: bigint;
55
+ }
56
+ /**
57
+ * Check for fill via ERC20 Transfer event logs over HTTP.
58
+ * Uses fresh providers rotated across RPCs. Subject to server-side
59
+ * caching (30-120s), kept as a fallback for when WS subscription fails.
60
+ */
61
+ export declare function checkTransferEvents(chainId: number, tokenAddress: string, walletAddress: string, fromBlock: number, rpcIndex: number): Promise<TransferEventResult>;
62
+ /**
63
+ * Check for fill via balance change using a fresh (uncached) provider.
64
+ * Each call cycles through a different RPC via rpcIndex.
65
+ */
66
+ export declare function checkBalanceChange(chainId: number, tokenAddress: string, walletAddress: string, preBalance: bigint, rpcIndex: number): Promise<BalanceChangeResult>;
67
+ /**
68
+ * Get the current block number on a chain using a fresh provider.
69
+ */
70
+ export declare function getCurrentBlockNumber(chainId: number): Promise<number>;