@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.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/backends/across.d.ts +10 -0
- package/dist/backends/across.js +285 -0
- package/dist/backends/debridge.d.ts +11 -0
- package/dist/backends/debridge.js +380 -0
- package/dist/backends/lifi.d.ts +19 -0
- package/dist/backends/lifi.js +295 -0
- package/dist/backends/persistence.d.ts +86 -0
- package/dist/backends/persistence.js +642 -0
- package/dist/backends/relay.d.ts +11 -0
- package/dist/backends/relay.js +292 -0
- package/dist/backends/squid.d.ts +31 -0
- package/dist/backends/squid.js +476 -0
- package/dist/backends/types.d.ts +125 -0
- package/dist/backends/types.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +154 -0
- package/dist/routing/engine.d.ts +49 -0
- package/dist/routing/engine.js +336 -0
- package/dist/tools/check-status.d.ts +3 -0
- package/dist/tools/check-status.js +93 -0
- package/dist/tools/execute-bridge.d.ts +3 -0
- package/dist/tools/execute-bridge.js +428 -0
- package/dist/tools/get-chains.d.ts +3 -0
- package/dist/tools/get-chains.js +162 -0
- package/dist/tools/get-quote.d.ts +3 -0
- package/dist/tools/get-quote.js +534 -0
- package/dist/tools/get-tokens.d.ts +3 -0
- package/dist/tools/get-tokens.js +128 -0
- package/dist/tools/help.d.ts +2 -0
- package/dist/tools/help.js +204 -0
- package/dist/tools/multi-quote.d.ts +3 -0
- package/dist/tools/multi-quote.js +310 -0
- package/dist/tools/onboard.d.ts +3 -0
- package/dist/tools/onboard.js +218 -0
- package/dist/tools/wallet.d.ts +14 -0
- package/dist/tools/wallet.js +744 -0
- package/dist/tools/xprt-farm.d.ts +3 -0
- package/dist/tools/xprt-farm.js +1308 -0
- package/dist/tools/xprt-rewards.d.ts +2 -0
- package/dist/tools/xprt-rewards.js +177 -0
- package/dist/tools/xprt-staking.d.ts +2 -0
- package/dist/tools/xprt-staking.js +565 -0
- package/dist/utils/chains.d.ts +22 -0
- package/dist/utils/chains.js +154 -0
- package/dist/utils/circuit-breaker.d.ts +64 -0
- package/dist/utils/circuit-breaker.js +160 -0
- package/dist/utils/evm.d.ts +18 -0
- package/dist/utils/evm.js +46 -0
- package/dist/utils/fill-detector.d.ts +70 -0
- package/dist/utils/fill-detector.js +298 -0
- package/dist/utils/gas-estimator.d.ts +67 -0
- package/dist/utils/gas-estimator.js +340 -0
- package/dist/utils/sanitize-error.d.ts +23 -0
- package/dist/utils/sanitize-error.js +101 -0
- package/dist/utils/token-registry.d.ts +70 -0
- package/dist/utils/token-registry.js +669 -0
- package/dist/utils/tokens.d.ts +17 -0
- package/dist/utils/tokens.js +37 -0
- package/dist/utils/tx-simulator.d.ts +27 -0
- package/dist/utils/tx-simulator.js +105 -0
- 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
|
+
}
|