@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Persistence Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # BridgeKitty 🐱
2
+
3
+ Cross-chain bridge aggregator MCP server for AI agents. One server, 5 bridge backends, best routes across EVM, Solana, and Cosmos chains.
4
+
5
+ BridgeKitty gives AI agents (Claude, Cursor, GPT, or any MCP-compatible AI) the ability to find and execute cross-chain bridge transfers — with automatic route optimization, fee comparison, balance checks, and safety warnings.
6
+
7
+ ## What's New in v0.3.0
8
+
9
+ - **`sign_and_send` parameter** — agents can now sign and broadcast transactions directly using locally-stored wallet keys
10
+ - **Full EVM signing support** — works with all EVM backends (Across, Relay, LI.FI, Squid, deBridge) + Persistence Interop (EIP-712)
11
+ - **Simulation fix** — ERC20 bridges now work on fresh wallets (previously blocked by premature simulation)
12
+ - **Solana signing** — coming in next release
13
+
14
+ <details>
15
+ <summary>What's New in v0.2.0</summary>
16
+
17
+ - **Solana support** — bidirectional bridging EVM ↔ Solana (native SOL delivery, not wrapped)
18
+ - **Cosmos support** — EVM → Persistence/Cosmos Hub via Squid (Axelar)
19
+ - **Protocol fee transparency** — deBridge fixFee, operating expenses, and total cost visible in every quote
20
+ - **Balance warnings** — warns when wallet can't cover bridge amount + protocol fees + gas
21
+ - **XPRT staking** — stake/unstake/claim rewards directly from the MCP server
22
+ - **Farming multiplier** — tracks your staking tier (1x → 3x → 5x) from the rewards API
23
+ - **Quote auto-refresh** — expired quotes automatically re-fetched on execute (60s expiry)
24
+ - **ERC-20 approvals** — always generated for token bridges (Relay + deBridge)
25
+ - **Bridge status tracking** — on-chain fallback when provider API hasn't indexed yet
26
+
27
+ </details>
28
+
29
+ ## Supported Bridges
30
+
31
+ | Backend | Type | Chains | Strength |
32
+ |---------|------|--------|----------|
33
+ | **deBridge (DLN)** | Direct | EVM + Solana | Fast intent-based fills, Solana support |
34
+ | **Relay** | Direct | EVM + Solana | No protocol fee, gas-optimized |
35
+ | **LI.FI** | Aggregator | EVM | Widest coverage (30+ bridges, any-to-any swap) |
36
+ | **Across** | Direct | EVM | Fastest fills (~6s), same-token bridging |
37
+ | **Squid (Axelar)** | Aggregator | EVM + Cosmos | Only option for EVM → Cosmos routes |
38
+
39
+ ### Bridge Directions
40
+
41
+ | Direction | Backends | Status |
42
+ |-----------|----------|--------|
43
+ | EVM → EVM | All 5 | ✅ Production |
44
+ | EVM → Solana | deBridge, Relay | ✅ Production |
45
+ | Solana → EVM | deBridge | ✅ Production |
46
+ | EVM → Cosmos | Squid | ✅ Production |
47
+
48
+ ## Quick Start
49
+
50
+ ### npx (zero install)
51
+
52
+ ```bash
53
+ npx @persistenceone/bridgekitty
54
+ ```
55
+
56
+ ### Claude Code
57
+
58
+ Add to your MCP config (`~/.claude/claude_code_config.json`):
59
+
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "bridgekitty": {
64
+ "command": "npx",
65
+ "args": ["@persistenceone/bridgekitty"]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### Cursor IDE
72
+
73
+ Add to Cursor's MCP settings (Settings > MCP Servers):
74
+
75
+ ```json
76
+ {
77
+ "bridgekitty": {
78
+ "command": "npx",
79
+ "args": ["@persistenceone/bridgekitty"]
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Claude Desktop
85
+
86
+ Add to `claude_desktop_config.json`:
87
+
88
+ ```json
89
+ {
90
+ "mcpServers": {
91
+ "bridgekitty": {
92
+ "command": "npx",
93
+ "args": ["@persistenceone/bridgekitty"]
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ## Wallet Setup
100
+
101
+ BridgeKitty can manage wallets for autonomous bridging. Run `wallet_setup` to create wallets for EVM, Cosmos, and Solana — or provide your own addresses in quotes.
102
+
103
+ Wallet config is stored in `~/.bridgekitty/.env` (or the directory you run from). Keys never leave the local machine.
104
+
105
+ | Variable | Description |
106
+ |----------|-------------|
107
+ | `PRIVATE_KEY` | EVM private key (hex) |
108
+ | `MNEMONIC` | BIP-39 mnemonic (derives EVM, Cosmos, Solana keys) |
109
+ | `SOLANA_PRIVATE_KEY` | Solana private key (base58) |
110
+
111
+ ## Transaction Signing
112
+
113
+ By default, `bridge_execute` returns unsigned transactions for the agent or user to sign externally.
114
+
115
+ Set `sign_and_send: true` to enable autonomous signing — BridgeKitty will use the wallet keys stored in `~/.bridgekitty/.env` to handle the full flow:
116
+
117
+ 1. **Approval** — sends ERC-20 approval transaction (if needed)
118
+ 2. **Re-build** — re-fetches the bridge transaction with updated nonce (if approval was sent)
119
+ 3. **Simulate** — runs `eth_estimateGas` pre-flight check
120
+ 4. **Sign** — signs the transaction with the local private key
121
+ 5. **Broadcast** — submits to the chain and returns the tx hash + explorer link
122
+
123
+ **Persistence Interop** uses EIP-712 typed data signing (Permit2 approval + on-chain initiate) instead of standard approve-and-send.
124
+
125
+ ### Optional API Keys
126
+
127
+ | Variable | Description |
128
+ |----------|-------------|
129
+ | `LIFI_API_KEY` | LI.FI API key (higher rate limits) |
130
+ | `DEBRIDGE_API_KEY` | deBridge API key |
131
+ | `SQUID_INTEGRATOR_ID` | Squid integrator ID |
132
+
133
+ ## MCP Tools
134
+
135
+ ### Core Bridge Tools
136
+
137
+ | Tool | Description |
138
+ |------|-------------|
139
+ | `bridge_get_quote` | Get competitive quotes from all backends. Shows fees, time estimates, balance warnings. |
140
+ | `bridge_execute` | Build transaction(s) from a quote. Handles approvals, auto-refreshes expired quotes. Set `sign_and_send: true` to auto-sign and broadcast. |
141
+ | `bridge_status` | Track bridge progress. On-chain fallback when API hasn't indexed yet. |
142
+ | `bridge_chains` | List supported chains with provider coverage. |
143
+ | `bridge_tokens` | Search tokens on a chain. |
144
+
145
+ ### Wallet Tools
146
+
147
+ | Tool | Description |
148
+ |------|-------------|
149
+ | `wallet_setup` | Create wallets for EVM, Cosmos, Solana from a single mnemonic. |
150
+ | `wallet_balance` | Check balances across all chains with USD prices (CoinGecko). |
151
+
152
+ ### XPRT Staking & Farming
153
+
154
+ | Tool | Description |
155
+ |------|-------------|
156
+ | `xprt_stake` | Stake XPRT to a validator (warns about 21-day unbonding). |
157
+ | `xprt_unstake` | Unstake XPRT (21-day unbonding period). |
158
+ | `xprt_claim_rewards` | Claim staking rewards. |
159
+ | `xprt_rewards_check` | Check farming rewards, multiplier tier, epoch status. |
160
+ | `xprt_farm_start` | Start automated BTC round-trip farming (cbBTC ↔ BTCB). |
161
+ | `xprt_farm_boost` | Buy + stake XPRT for multiplier boost (1x → 3x → 5x). |
162
+ | `bridgekitty_help` | Full docs on farming tiers, multipliers, and strategy. |
163
+
164
+ ## Example: Bridge USDC from Base to Arbitrum
165
+
166
+ ### Default (unsigned transactions)
167
+
168
+ ```
169
+ Agent: "Bridge 100 USDC from Base to Arbitrum"
170
+
171
+ → bridge_get_quote: Gets quotes from deBridge, Relay, LI.FI, Across
172
+ → Shows: best rate, fees, estimated time, balance check
173
+ → bridge_execute: Builds approval tx + bridge tx
174
+ → Agent signs and sends both transactions
175
+ → bridge_status: Tracks until destination confirmed
176
+ ```
177
+
178
+ ### With sign_and_send (autonomous signing)
179
+
180
+ ```
181
+ Agent: "Bridge 100 USDC from Base to Arbitrum"
182
+
183
+ → bridge_get_quote: Gets quotes from all backends
184
+ → bridge_execute with sign_and_send: true
185
+ → Auto-signs approval tx + bridge tx using local wallet keys
186
+ → Returns tx hash + explorer link
187
+ → bridge_status: Tracks until destination confirmed
188
+ ```
189
+
190
+ ## Architecture
191
+
192
+ ```
193
+ Agent → MCP Tools → Routing Engine → [deBridge, Relay, LI.FI, Across, Squid]
194
+
195
+ Quote Cache (60s) + Circuit Breaker
196
+
197
+ Best Quote → buildTransaction
198
+
199
+ ┌──────────┴──────────┐
200
+ ↓ ↓
201
+ Unsigned TX Signed + Broadcast
202
+ (default) (sign_and_send)
203
+ ```
204
+
205
+ - **Routing Engine:** Parallel quotes from all backends, ranked by output amount
206
+ - **Circuit Breaker:** Auto-skips failing backends, gradual recovery
207
+ - **Token Registry:** 45+ verified tokens with canonical addresses per chain
208
+ - **Gas Estimator:** Chain-aware gas cost estimation with multi-RPC failover
209
+ - **Balance Checker:** Validates token + native balance for fees before execution
210
+ - **Fee Transparency:** Protocol fees (deBridge fixFee, operating expenses) surfaced in every quote
211
+
212
+ ## Security
213
+
214
+ - Exact-amount approvals only (never unlimited)
215
+ - Transaction simulation before execution
216
+ - Verified token registry prevents address spoofing
217
+ - No private keys in MCP protocol — agents sign transactions externally
218
+ - `sign_and_send` uses locally-stored keys only (never transmitted over the network)
219
+ - Circuit breaker prevents cascading failures
220
+ - Error messages sanitized (no key/path leakage)
221
+ - `.env` file permission checks + overwrite protection
222
+
223
+ ## Known Limitations
224
+
225
+ - **Solana → EVM** returns a serialized transaction for external signing (no auto-execute)
226
+ - **Relay status tracking** may show "unknown" for completed cross-chain bridges
227
+ - **Solana SPL tokens** not yet shown in `wallet_balance` (only native SOL)
228
+ - **Cosmos → EVM** bridging not yet supported (only EVM → Cosmos)
229
+
230
+ ## License
231
+
232
+ MIT
@@ -0,0 +1,10 @@
1
+ import type { BridgeBackend, BridgeQuote, BridgeStatus, ChainInfo, QuoteParams, TransactionRequest } from "./types.js";
2
+ export declare class AcrossBackend implements BridgeBackend {
3
+ name: string;
4
+ private referrer?;
5
+ constructor(referrer?: string);
6
+ getQuote(params: QuoteParams): Promise<BridgeQuote | null>;
7
+ buildTransaction(quote: BridgeQuote): Promise<TransactionRequest>;
8
+ getStatus(trackingId: string, meta?: Record<string, string>): Promise<BridgeStatus>;
9
+ getSupportedChains(): Promise<ChainInfo[]>;
10
+ }
@@ -0,0 +1,285 @@
1
+ import { Interface } from "ethers";
2
+ import { formatTokenAmount } from "../utils/tokens.js";
3
+ import { getAllChains } from "../utils/chains.js";
4
+ import { buildApproveData, isNativeToken } from "../utils/evm.js";
5
+ import { estimateGasCostUsd, getGasUnits } from "../utils/gas-estimator.js";
6
+ import { lookupByAddress } from "../utils/token-registry.js";
7
+ import { sanitizeError } from "../utils/sanitize-error.js";
8
+ /** WETH addresses by chain — Across requires WETH address for native ETH bridging */
9
+ const WETH_BY_CHAIN = {
10
+ 1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // Ethereum
11
+ 10: "0x4200000000000000000000000000000000000006", // Optimism
12
+ 56: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", // BSC (WBNB)
13
+ 137: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", // Polygon (WMATIC)
14
+ 8453: "0x4200000000000000000000000000000000000006", // Base
15
+ 42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", // Arbitrum
16
+ 43114: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", // Avalanche (WAVAX)
17
+ 59144: "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f", // Linea (WETH)
18
+ 534352: "0x5300000000000000000000000000000000000004", // Scroll (WETH)
19
+ 324: "0x5AEa5775959fBC2557Cc8789bC1bf90A239D9a91", // zkSync Era (WETH)
20
+ };
21
+ /** Across V3 SpokePool depositV3 ABI fragment */
22
+ const SPOKE_POOL_ABI = new Interface([
23
+ "function depositV3(address depositor, address recipient, address inputToken, address outputToken, uint256 inputAmount, uint256 outputAmount, uint256 destinationChainId, address exclusiveRelayer, uint32 quoteTimestamp, uint32 fillDeadline, uint32 exclusivityDeadline, bytes message)",
24
+ ]);
25
+ const BASE_URL = "https://app.across.to/api";
26
+ const TIMEOUT_MS = 15_000;
27
+ async function fetchJson(url, init) {
28
+ const controller = new AbortController();
29
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
30
+ try {
31
+ const res = await fetch(url, { ...init, signal: controller.signal });
32
+ if (!res.ok) {
33
+ const text = await res.text().catch(() => "");
34
+ throw new Error(`Across ${res.status}: ${text.slice(0, 200)}`);
35
+ }
36
+ return res.json();
37
+ }
38
+ finally {
39
+ clearTimeout(timer);
40
+ }
41
+ }
42
+ // buildApproveData imported from ../utils/evm.js
43
+ // Across V3 SpokePool addresses per chain (NEW-LOW-002)
44
+ // These addresses are from Across Protocol V3 deployment.
45
+ // Verified: 2025-02-25. If Across upgrades to V4 or redeploys contracts,
46
+ // these addresses MUST be updated. Check: https://docs.across.to/reference/contract-addresses
47
+ const SPOKE_POOLS = {
48
+ 1: "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5",
49
+ 10: "0x6f26Bf09B1C792e3228e5467807a900A503c0281",
50
+ 137: "0x9295ee1d8C5b022Be115A2AD3c30C72E34e7F096",
51
+ 42161: "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A",
52
+ 8453: "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64",
53
+ 59144: "0x7E63A5f1a8F0B4d0934B2f2327DAED3F6bb2ee75",
54
+ 534352: "0x3baD7AD0728f9917d1Bf08af5782dCbD516cDd96",
55
+ 324: "0xE0B015E54d54fc84a6cB9B666099c46adE3335fF",
56
+ };
57
+ export class AcrossBackend {
58
+ name = "across";
59
+ referrer;
60
+ constructor(referrer) {
61
+ this.referrer = referrer;
62
+ }
63
+ async getQuote(params) {
64
+ try {
65
+ // Across only supports same-token bridging (e.g. USDC→USDC across chains).
66
+ // Skip cross-token swaps — those should go through aggregators like LI.FI.
67
+ // NOTE: We compare by symbol, not address, because the same token (e.g. USDC)
68
+ // has different contract addresses on different chains.
69
+ if (params.fromTokenAddress.toLowerCase() !== params.toTokenAddress.toLowerCase()) {
70
+ const fromToken = lookupByAddress(params.fromTokenAddress, params.fromChainId);
71
+ const toToken = lookupByAddress(params.toTokenAddress, params.toChainId);
72
+ // If both are known tokens with the same symbol, Across can bridge them.
73
+ // If either is unknown or symbols differ, skip — let aggregators handle it.
74
+ if (!fromToken || !toToken || fromToken.symbol !== toToken.symbol) {
75
+ return null;
76
+ }
77
+ }
78
+ // Across uses suggested-fees to get the fee structure for a route
79
+ const url = new URL(`${BASE_URL}/suggested-fees`);
80
+ url.searchParams.set("originChainId", String(params.fromChainId));
81
+ url.searchParams.set("destinationChainId", String(params.toChainId));
82
+ // Across needs WETH address for native tokens
83
+ const isNative = isNativeToken(params.fromTokenAddress);
84
+ const acrossToken = isNative
85
+ ? (WETH_BY_CHAIN[params.fromChainId] ?? params.fromTokenAddress)
86
+ : params.fromTokenAddress;
87
+ url.searchParams.set("token", acrossToken);
88
+ url.searchParams.set("amount", params.amountRaw);
89
+ if (this.referrer) {
90
+ url.searchParams.set("referrer", this.referrer);
91
+ }
92
+ const data = await fetchJson(url.toString());
93
+ if (!data.totalRelayFee)
94
+ return null;
95
+ const inputBig = BigInt(params.amountRaw);
96
+ const totalFeeBig = BigInt(data.totalRelayFee.total ?? "0");
97
+ const outputBig = inputBig - totalFeeBig;
98
+ if (outputBig <= 0n)
99
+ return null;
100
+ const outputRaw = outputBig.toString();
101
+ // Use token decimals from the API response if available, fall back to params or registry
102
+ const decimals = data.inputToken?.decimals ?? params.fromTokenDecimals ?? 6;
103
+ // Across doesn't provide USD fee estimates — we get fee in token units.
104
+ // Convert to human-readable token amount. For stablecoins this ≈ USD,
105
+ // for other tokens it's an approximation (would need price oracle for true USD).
106
+ const feeTokenAmount = Number(formatTokenAmount(totalFeeBig.toString(), decimals));
107
+ // Use token amount as fee estimate — close enough for stablecoins,
108
+ // and we label it clearly in the fee breakdown.
109
+ const feeUsd = feeTokenAmount;
110
+ const estimatedFillTime = data.estimatedFillTimeSec ?? 120;
111
+ // Estimate source chain gas cost (chain-aware)
112
+ const gasUnits = getGasUnits("across", params.fromChainId);
113
+ const gasEstimate = await estimateGasCostUsd(params.fromChainId, gasUnits);
114
+ const gasCostUsd = gasEstimate?.costUsd ?? null;
115
+ return {
116
+ backendName: "across",
117
+ provider: "Across (direct)",
118
+ outputAmount: formatTokenAmount(outputRaw, decimals),
119
+ outputAmountRaw: outputRaw,
120
+ // Across is intent-based with deterministic pricing — min = estimated
121
+ minOutputAmount: formatTokenAmount(outputRaw, decimals),
122
+ minOutputAmountRaw: outputRaw,
123
+ outputDecimals: decimals,
124
+ estimatedGasCostUsd: gasCostUsd,
125
+ usingFallbackPrices: gasEstimate?.usingFallbackPrices,
126
+ estimatedFeeUsd: gasCostUsd !== null ? Math.round((feeUsd + gasCostUsd) * 100) / 100 : null,
127
+ feeBreakdown: {
128
+ gasCostUsd,
129
+ protocolFeeUsd: Math.round(feeUsd * 100) / 100,
130
+ integratorFeeUsd: 0,
131
+ integratorFeePercent: null,
132
+ totalFeeUsd: gasCostUsd !== null ? Math.round((feeUsd + gasCostUsd) * 100) / 100 : null,
133
+ },
134
+ estimatedTimeSeconds: estimatedFillTime,
135
+ route: `Across Protocol (fast bridge)`,
136
+ quoteData: {
137
+ fees: data,
138
+ params: {
139
+ fromChainId: params.fromChainId,
140
+ toChainId: params.toChainId,
141
+ fromTokenAddress: params.fromTokenAddress,
142
+ toTokenAddress: params.toTokenAddress,
143
+ amountRaw: params.amountRaw,
144
+ fromAddress: params.fromAddress,
145
+ toAddress: params.toAddress || params.fromAddress,
146
+ outputRaw,
147
+ },
148
+ spokePool: SPOKE_POOLS[params.fromChainId],
149
+ timestamp: data.timestamp ?? Math.floor(Date.now() / 1000),
150
+ exclusiveRelayer: data.exclusiveRelayer ?? "0x0000000000000000000000000000000000000000",
151
+ exclusivityDeadline: data.exclusivityDeadline ?? 0,
152
+ },
153
+ // Across quotes are based on a specific timestamp. The fee structure
154
+ // is valid for a limited window. Use the shorter of: the API-provided
155
+ // exclusivityDeadline or a conservative 60s default.
156
+ // exclusivityDeadline from the API is a small relative offset (e.g. 5 seconds),
157
+ // NOT an epoch timestamp. Use a conservative 60s TTL for quote validity.
158
+ expiresAt: Date.now() + 60_000,
159
+ };
160
+ }
161
+ catch (err) {
162
+ console.error("[across] quote error:", err.message);
163
+ return null;
164
+ }
165
+ }
166
+ async buildTransaction(quote) {
167
+ const qd = quote.quoteData;
168
+ const p = qd.params;
169
+ const spokePool = qd.spokePool;
170
+ if (!spokePool) {
171
+ throw new Error(`Across: no SpokePool address for chain ${p.fromChainId}`);
172
+ }
173
+ const isNative = isNativeToken(p.fromTokenAddress);
174
+ // Build depositV3 calldata using ethers Interface (V3-LOW-002)
175
+ const calldata = SPOKE_POOL_ABI.encodeFunctionData("depositV3", [
176
+ p.fromAddress, // depositor
177
+ p.toAddress || p.fromAddress, // recipient
178
+ isNative ? (WETH_BY_CHAIN[p.fromChainId] ?? p.fromTokenAddress) : p.fromTokenAddress, // inputToken (WETH for native)
179
+ isNativeToken(p.toTokenAddress) ? (WETH_BY_CHAIN[p.toChainId] ?? p.toTokenAddress) : p.toTokenAddress, // outputToken (WETH for native)
180
+ BigInt(p.amountRaw), // inputAmount
181
+ BigInt(p.outputRaw), // outputAmount
182
+ BigInt(p.toChainId), // destinationChainId
183
+ qd.exclusiveRelayer ?? "0x0000000000000000000000000000000000000000", // exclusiveRelayer
184
+ qd.timestamp ?? Math.floor(Date.now() / 1000), // quoteTimestamp
185
+ Math.floor(Date.now() / 1000) + 3600, // fillDeadline (1 hour)
186
+ qd.exclusivityDeadline ?? 0, // exclusivityDeadline (relative offset from quoteTimestamp, per Across V3 contract)
187
+ "0x", // message (empty)
188
+ ]);
189
+ const result = {
190
+ to: spokePool,
191
+ data: calldata,
192
+ value: isNative ? `0x${BigInt(p.amountRaw).toString(16)}` : "0x0",
193
+ chainId: p.fromChainId,
194
+ provider: "across",
195
+ trackingId: `across:${p.fromChainId}:${Date.now()}`,
196
+ };
197
+ // Add approval for ERC20 tokens
198
+ if (!isNative) {
199
+ result.approvalTx = {
200
+ to: p.fromTokenAddress,
201
+ data: buildApproveData(spokePool, p.amountRaw),
202
+ value: "0x0",
203
+ chainId: p.fromChainId,
204
+ };
205
+ }
206
+ return result;
207
+ }
208
+ async getStatus(trackingId, meta) {
209
+ try {
210
+ const txHash = meta?.txHash;
211
+ if (!txHash) {
212
+ return {
213
+ state: "unknown",
214
+ humanReadable: "No transaction hash provided for Across status check",
215
+ provider: "across",
216
+ elapsed: 0,
217
+ };
218
+ }
219
+ const fromChain = meta?.fromChain ?? "1";
220
+ const url = new URL(`${BASE_URL}/deposit/status`);
221
+ url.searchParams.set("originChainId", fromChain);
222
+ url.searchParams.set("depositTxHash", txHash);
223
+ const data = await fetchJson(url.toString());
224
+ const stateMap = {
225
+ pending: "pending",
226
+ filled: "completed",
227
+ expired: "failed",
228
+ slowfill: "in_progress",
229
+ };
230
+ return {
231
+ state: stateMap[data.status] ?? "in_progress",
232
+ humanReadable: `Across bridge: ${data.status ?? "unknown"}`,
233
+ sourceTxHash: txHash,
234
+ destTxHash: data.fillTx,
235
+ provider: "across",
236
+ elapsed: 0,
237
+ };
238
+ }
239
+ catch (err) {
240
+ return {
241
+ state: "unknown",
242
+ humanReadable: `Status check failed: ${sanitizeError(err)}`,
243
+ provider: "across",
244
+ elapsed: 0,
245
+ };
246
+ }
247
+ }
248
+ async getSupportedChains() {
249
+ try {
250
+ const data = await fetchJson(`${BASE_URL}/available-routes`);
251
+ if (Array.isArray(data)) {
252
+ const chainIds = new Set();
253
+ for (const route of data) {
254
+ if (route.originChainId)
255
+ chainIds.add(route.originChainId);
256
+ if (route.destinationChainId)
257
+ chainIds.add(route.destinationChainId);
258
+ }
259
+ const chains = getAllChains();
260
+ return Array.from(chainIds).map((id) => {
261
+ const known = chains.find((c) => c.id === id);
262
+ return {
263
+ id,
264
+ name: known?.name ?? `Chain ${id}`,
265
+ key: known?.key ?? `chain-${id}`,
266
+ providers: ["across"],
267
+ };
268
+ });
269
+ }
270
+ }
271
+ catch {
272
+ // Fallback
273
+ }
274
+ return Object.keys(SPOKE_POOLS).map((id) => {
275
+ const chains = getAllChains();
276
+ const known = chains.find((c) => c.id === Number(id));
277
+ return {
278
+ id: Number(id),
279
+ name: known?.name ?? `Chain ${id}`,
280
+ key: known?.key ?? `chain-${id}`,
281
+ providers: ["across"],
282
+ };
283
+ });
284
+ }
285
+ }
@@ -0,0 +1,11 @@
1
+ import type { BridgeBackend, BridgeQuote, BridgeStatus, ChainInfo, QuoteParams, TransactionRequest } from "./types.js";
2
+ export declare class DeBridgeBackend implements BridgeBackend {
3
+ name: string;
4
+ private affiliateFeePercent?;
5
+ private affiliateFeeRecipient?;
6
+ constructor(affiliateFeePercent?: string, affiliateFeeRecipient?: string);
7
+ getQuote(params: QuoteParams): Promise<BridgeQuote | null>;
8
+ buildTransaction(quote: BridgeQuote): Promise<TransactionRequest>;
9
+ getStatus(trackingId: string, meta?: Record<string, string>): Promise<BridgeStatus>;
10
+ getSupportedChains(): Promise<ChainInfo[]>;
11
+ }