@exagent/agent 0.3.5 → 0.3.7

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 (78) hide show
  1. package/dist/chunk-7UGLJO6W.js +6392 -0
  2. package/dist/chunk-EHAOPCTJ.js +6406 -0
  3. package/dist/chunk-FGMXTW5I.js +6540 -0
  4. package/dist/chunk-IVA2SCSN.js +6756 -0
  5. package/dist/chunk-JHXCSGPC.js +6352 -0
  6. package/dist/chunk-V6O4UXVN.js +6345 -0
  7. package/dist/chunk-ZRAOPQQW.js +6406 -0
  8. package/dist/cli.js +40 -98
  9. package/dist/index.d.ts +24 -2
  10. package/dist/index.js +1 -1
  11. package/package.json +17 -14
  12. package/.turbo/turbo-build.log +0 -17
  13. package/src/bridge/across.ts +0 -240
  14. package/src/bridge/bridge-manager.ts +0 -87
  15. package/src/bridge/index.ts +0 -9
  16. package/src/bridge/types.ts +0 -77
  17. package/src/chains.ts +0 -105
  18. package/src/cli.ts +0 -244
  19. package/src/config.ts +0 -499
  20. package/src/diagnostics.ts +0 -335
  21. package/src/index.ts +0 -98
  22. package/src/llm/anthropic.ts +0 -63
  23. package/src/llm/base.ts +0 -264
  24. package/src/llm/deepseek.ts +0 -48
  25. package/src/llm/google.ts +0 -63
  26. package/src/llm/groq.ts +0 -48
  27. package/src/llm/index.ts +0 -42
  28. package/src/llm/mistral.ts +0 -48
  29. package/src/llm/ollama.ts +0 -52
  30. package/src/llm/openai.ts +0 -51
  31. package/src/llm/together.ts +0 -48
  32. package/src/llm-providers.ts +0 -100
  33. package/src/logger.ts +0 -137
  34. package/src/paper/executor.ts +0 -201
  35. package/src/paper/index.ts +0 -1
  36. package/src/perp/client.ts +0 -200
  37. package/src/perp/index.ts +0 -12
  38. package/src/perp/msgpack.ts +0 -272
  39. package/src/perp/orders.ts +0 -234
  40. package/src/perp/positions.ts +0 -126
  41. package/src/perp/signer.ts +0 -277
  42. package/src/perp/types.ts +0 -192
  43. package/src/perp/websocket.ts +0 -274
  44. package/src/position-tracker.ts +0 -243
  45. package/src/prediction/client.ts +0 -281
  46. package/src/prediction/index.ts +0 -3
  47. package/src/prediction/order-manager.ts +0 -297
  48. package/src/prediction/types.ts +0 -151
  49. package/src/relay.ts +0 -254
  50. package/src/runtime.ts +0 -1755
  51. package/src/scrub-secrets.ts +0 -39
  52. package/src/setup.ts +0 -384
  53. package/src/signal.ts +0 -212
  54. package/src/spot/aerodrome.ts +0 -158
  55. package/src/spot/client.ts +0 -138
  56. package/src/spot/index.ts +0 -11
  57. package/src/spot/swap-manager.ts +0 -219
  58. package/src/spot/types.ts +0 -203
  59. package/src/spot/uniswap.ts +0 -150
  60. package/src/store.ts +0 -50
  61. package/src/strategy/index.ts +0 -2
  62. package/src/strategy/loader.ts +0 -191
  63. package/src/strategy/templates.ts +0 -125
  64. package/src/trading/index.ts +0 -2
  65. package/src/trading/market.ts +0 -120
  66. package/src/trading/risk.ts +0 -107
  67. package/src/ui.ts +0 -75
  68. package/test-bridge-arb-to-base.mjs +0 -223
  69. package/test-funded-check.mjs +0 -79
  70. package/test-funded-phase19.mjs +0 -933
  71. package/test-hl-deposit-recover.mjs +0 -281
  72. package/test-hl-withdraw.mjs +0 -372
  73. package/test-live-signing.mjs +0 -374
  74. package/test-phase7.mjs +0 -416
  75. package/test-recover-arb.mjs +0 -206
  76. package/test-spot-bridge.mjs +0 -248
  77. package/test-wallet-setup.mjs +0 -126
  78. package/tsconfig.json +0 -8
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  updateSecureStore,
10
10
  writeConfigFile,
11
11
  writeSampleConfig
12
- } from "./chunk-WTECTX2Z.js";
12
+ } from "./chunk-IVA2SCSN.js";
13
13
 
14
14
  // src/cli.ts
15
15
  import { Command } from "commander";
@@ -80,85 +80,13 @@ function printError(message) {
80
80
  }
81
81
 
82
82
  // src/llm-providers.ts
83
- var LLM_PROVIDERS = [
84
- {
85
- id: "openai",
86
- label: "OpenAI",
87
- models: [
88
- { id: "gpt-5.2", label: "GPT-5.2" },
89
- { id: "gpt-5.2-pro", label: "GPT-5.2 Pro" },
90
- { id: "gpt-5-mini", label: "GPT-5 Mini" },
91
- { id: "gpt-5-nano", label: "GPT-5 Nano" },
92
- { id: "gpt-4o", label: "GPT-4o" },
93
- { id: "gpt-4o-mini", label: "GPT-4o Mini" }
94
- ]
95
- },
96
- {
97
- id: "anthropic",
98
- label: "Anthropic",
99
- models: [
100
- { id: "claude-opus-4-6", label: "Claude Opus 4.6" },
101
- { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
102
- { id: "claude-haiku-4-5", label: "Claude Haiku 4.5" }
103
- ]
104
- },
105
- {
106
- id: "google",
107
- label: "Google",
108
- models: [
109
- { id: "gemini-3-pro", label: "Gemini 3 Pro" },
110
- { id: "gemini-3-flash", label: "Gemini 3 Flash" },
111
- { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
112
- { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
113
- { id: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite" }
114
- ]
115
- },
116
- {
117
- id: "deepseek",
118
- label: "DeepSeek",
119
- models: [
120
- { id: "deepseek-chat", label: "DeepSeek Chat" },
121
- { id: "deepseek-reasoner", label: "DeepSeek Reasoner" }
122
- ]
123
- },
124
- {
125
- id: "mistral",
126
- label: "Mistral",
127
- models: [
128
- { id: "mistral-large-latest", label: "Mistral Large" },
129
- { id: "mistral-small-latest", label: "Mistral Small" }
130
- ]
131
- },
132
- {
133
- id: "groq",
134
- label: "Groq",
135
- models: [
136
- { id: "llama-3.3-70b-versatile", label: "Llama 3.3 70B" },
137
- { id: "llama-3.1-8b-instant", label: "Llama 3.1 8B" },
138
- { id: "mixtral-8x7b-32768", label: "Mixtral 8x7B" }
139
- ]
140
- },
141
- {
142
- id: "together",
143
- label: "Together",
144
- models: [
145
- { id: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", label: "Llama 3.1 70B" },
146
- { id: "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", label: "Llama 3.1 8B" }
147
- ]
148
- },
149
- {
150
- id: "ollama",
151
- label: "Ollama (local)",
152
- models: [
153
- { id: "llama3.1", label: "Llama 3.1" },
154
- { id: "mistral", label: "Mistral" },
155
- { id: "custom", label: "Custom (type model name)" }
156
- ]
157
- }
158
- ];
159
- function getProvider(id) {
160
- return LLM_PROVIDERS.find((p) => p.id === id);
161
- }
83
+ import {
84
+ LLM_PROVIDERS,
85
+ getDefaultModel,
86
+ getProvider,
87
+ getProviderIds,
88
+ providerRequiresApiKey
89
+ } from "@exagent/sdk";
162
90
 
163
91
  // src/setup.ts
164
92
  function expandHomeDir(path) {
@@ -261,7 +189,9 @@ async function setupLlm(config) {
261
189
  const apiKey2 = process.env.EXAGENT_LLM_KEY;
262
190
  if (!provider2) throw new Error("EXAGENT_LLM_PROVIDER required in non-interactive mode");
263
191
  if (!model2) throw new Error("EXAGENT_LLM_MODEL required in non-interactive mode");
264
- if (!apiKey2) throw new Error("EXAGENT_LLM_KEY required in non-interactive mode");
192
+ if (providerRequiresApiKey(provider2) && !apiKey2) {
193
+ throw new Error("EXAGENT_LLM_KEY required in non-interactive mode");
194
+ }
265
195
  printDone("LLM configured");
266
196
  return { provider: provider2, model: model2, apiKey: apiKey2 };
267
197
  }
@@ -276,7 +206,7 @@ async function setupLlm(config) {
276
206
  const provider = selected;
277
207
  const defaultModel = config.llm?.model;
278
208
  const providerInfo = getProvider(provider);
279
- const modelOptions = providerInfo ? providerInfo.models.map((m) => ({ value: m.id, label: m.label })) : [{ value: defaultModel || "gpt-4o", label: defaultModel || "gpt-4o" }];
209
+ const modelOptions = providerInfo ? providerInfo.models.map((m) => ({ value: m.id, label: m.label })) : [{ value: defaultModel || getDefaultModel("openai"), label: defaultModel || getDefaultModel("openai") }];
280
210
  const selectedModel = await clack.select({
281
211
  message: "LLM model:",
282
212
  options: modelOptions,
@@ -284,11 +214,17 @@ async function setupLlm(config) {
284
214
  });
285
215
  if (clack.isCancel(selectedModel)) cancelled();
286
216
  const model = selectedModel;
287
- const apiKey = await clack.password({
288
- message: "LLM API key:",
289
- validate: (val) => validateLlmKeyFormat(provider, val)
290
- });
291
- if (clack.isCancel(apiKey)) cancelled();
217
+ let apiKey;
218
+ if (providerRequiresApiKey(provider)) {
219
+ const enteredApiKey = await clack.password({
220
+ message: "LLM API key:",
221
+ validate: (val) => validateLlmKeyFormat(provider, val)
222
+ });
223
+ if (clack.isCancel(enteredApiKey)) cancelled();
224
+ apiKey = enteredApiKey;
225
+ } else {
226
+ printInfo("Ollama uses your local server; no API key needed.");
227
+ }
292
228
  printDone("LLM configured");
293
229
  return { provider, model, apiKey };
294
230
  }
@@ -423,7 +359,7 @@ async function ensureLocalSetup(configPath) {
423
359
  // src/cli.ts
424
360
  import * as clack2 from "@clack/prompts";
425
361
  var program = new Command();
426
- program.name("exagent").description("Exagent \u2014 LLM trading agent runtime").version("0.3.5");
362
+ program.name("exagent").description("Exagent \u2014 LLM trading agent runtime").version("0.3.7");
427
363
  program.command("init").description("Create a sample agent configuration file").option("--agent-id <id>", "Agent ID (from dashboard)", "my-agent").option("--api-url <url>", "API server URL", "http://localhost:3002").option("--config <path>", "Config file path", "agent-config.json").action((opts) => {
428
364
  printBanner();
429
365
  writeSampleConfig(opts.agentId, opts.apiUrl, opts.config);
@@ -558,16 +494,22 @@ program.command("config").description("Change LLM provider, model, or API key").
558
494
  }
559
495
  newModel = selectedModel;
560
496
  }
561
- const newKey = await clack2.password({
562
- message: "New LLM API key:",
563
- validate: (val) => {
564
- if (!val?.trim()) return "API key is required.";
565
- if (val.length < 10) return "API key seems too short.";
497
+ let newKey;
498
+ if (providerRequiresApiKey(newProvider)) {
499
+ const enteredKey = await clack2.password({
500
+ message: "New LLM API key:",
501
+ validate: (val) => {
502
+ if (!val?.trim()) return "API key is required.";
503
+ if (val.length < 10) return "API key seems too short.";
504
+ }
505
+ });
506
+ if (clack2.isCancel(enteredKey)) {
507
+ clack2.cancel("Cancelled.");
508
+ process.exit(0);
566
509
  }
567
- });
568
- if (clack2.isCancel(newKey)) {
569
- clack2.cancel("Cancelled.");
570
- process.exit(0);
510
+ newKey = enteredKey;
511
+ } else {
512
+ printInfo("Ollama uses your local server; no API key needed.");
571
513
  }
572
514
  updateSecureStore(secureStorePath, password3, { llmApiKey: newKey });
573
515
  const updatedConfig = readConfigFile(opts.config);
@@ -581,7 +523,7 @@ program.command("config").description("Change LLM provider, model, or API key").
581
523
  printSuccess("Updated", [
582
524
  `${pc.dim("Provider:")} ${pc.cyan(newProvider)}`,
583
525
  `${pc.dim("Model:")} ${pc.cyan(newModel)}`,
584
- `${pc.dim("API key:")} ${pc.dim(`${newKey.slice(0, 7)}...${newKey.slice(-4)}`)}`,
526
+ `${pc.dim("API key:")} ${newKey ? pc.dim(`${newKey.slice(0, 7)}...${newKey.slice(-4)}`) : pc.dim("not required")}`,
585
527
  "",
586
528
  `Run ${pc.cyan("npx exagent run")} to start with the new configuration.`
587
529
  ]);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LLMProvider, RelayCommand, AgentStatusPayload, MessageType, MessageLevel, TradeSignal, StrategyStore, PositionSummary, TrackedPosition, TradeRecord, LLMAdapter, LLMConfig, LLMMessage, LLMResponse, LLMMetadata, StrategyFunction, StrategyTemplate, RiskParams, MarketData, OHLCV, PaperTrade, PaperMetrics } from '@exagent/sdk';
1
+ import { LLMProvider, StrategyProvenance, StrategyPermissions, RelayCommand, AgentStatusPayload, MessageType, MessageLevel, TradeSignal, StrategyStore, PositionSummary, TrackedPosition, TradeRecord, LLMAdapter, LLMConfig, LLMMessage, LLMResponse, LLMMetadata, StrategyFunction, StrategyTemplate, RiskParams, MarketData, OHLCV, PaperTrade, PaperMetrics } from '@exagent/sdk';
2
2
  import { WalletClient, PublicClient, Transport, Chain as Chain$1, Account } from 'viem';
3
3
  import { Chain } from 'viem/chains';
4
4
 
@@ -22,6 +22,8 @@ interface RuntimeConfig {
22
22
  code?: string;
23
23
  template?: string;
24
24
  venues?: string[];
25
+ provenance?: StrategyProvenance;
26
+ permissions?: StrategyPermissions;
25
27
  prompt?: {
26
28
  name?: string;
27
29
  systemPrompt: string;
@@ -137,6 +139,8 @@ declare class AgentRuntime {
137
139
  private buildRuntimeVenuesFromSelection;
138
140
  private buildStrategyFallbackPrompt;
139
141
  private extractStrategyConfigFromAgentConfig;
142
+ private extractStrategyProvenance;
143
+ private extractStrategyPermissions;
140
144
  private applyExecutionMode;
141
145
  private initializeVenues;
142
146
  private startTrading;
@@ -436,6 +440,8 @@ declare function loadStrategy(config: {
436
440
  systemPrompt: string;
437
441
  venues?: string[];
438
442
  };
443
+ provenance?: StrategyProvenance;
444
+ permissions?: StrategyPermissions;
439
445
  }): Promise<StrategyFunction>;
440
446
 
441
447
  declare function getTemplate(id: string): StrategyTemplate | undefined;
@@ -456,6 +462,7 @@ declare class RiskManager {
456
462
  getDailyPnL(): number;
457
463
  getDailyLossLimit(): number;
458
464
  private resetIfNewDay;
465
+ private validateUpdate;
459
466
  }
460
467
 
461
468
  declare class MarketDataService implements MarketData {
@@ -559,6 +566,7 @@ interface PerpConfig {
559
566
  wsUrl: string;
560
567
  maxLeverage: number;
561
568
  maxNotionalUSD: number;
569
+ marketOrderSlippageBps?: number;
562
570
  allowedInstruments?: string[];
563
571
  }
564
572
  type PerpAction = 'open_long' | 'open_short' | 'close_long' | 'close_short' | 'hold';
@@ -847,6 +855,13 @@ declare class HyperliquidSigner {
847
855
  * No builder fee — private group, not needed.
848
856
  */
849
857
 
858
+ interface CancelAllResult {
859
+ success: boolean;
860
+ total: number;
861
+ cancelled: number;
862
+ failed: number;
863
+ errors: string[];
864
+ }
850
865
  declare class HyperliquidOrderManager {
851
866
  private readonly client;
852
867
  private readonly signer;
@@ -854,11 +869,15 @@ declare class HyperliquidOrderManager {
854
869
  constructor(client: HyperliquidClient, signer: HyperliquidSigner, config: PerpConfig);
855
870
  placeOrder(signal: PerpTradeSignal): Promise<OrderResult>;
856
871
  cancelOrder(instrument: string, orderId: number): Promise<boolean>;
857
- closePosition(instrument: string, positionSize: number): Promise<OrderResult>;
872
+ cancelAllOrders(userAddress?: `0x${string}`): Promise<CancelAllResult>;
873
+ closePosition(instrument: string, positionSize: number, referencePrice?: number): Promise<OrderResult>;
858
874
  updateLeverage(instrument: string, leverage: number, isCross?: boolean): Promise<boolean>;
859
875
  private getMarketPrice;
876
+ private getReferencePrice;
877
+ private formatDecimal;
860
878
  private parseOrderResponse;
861
879
  private exchangeRequest;
880
+ private signedExchangeRequest;
862
881
  }
863
882
 
864
883
  /**
@@ -964,6 +983,7 @@ interface PredictionTradeSignal {
964
983
  amount: number;
965
984
  limitPrice: number;
966
985
  orderType: 'limit' | 'market';
986
+ marketOrderType?: 'FOK' | 'FAK';
967
987
  confidence: number;
968
988
  reasoning?: string;
969
989
  }
@@ -1078,6 +1098,8 @@ declare class PolymarketClient {
1078
1098
  tokenId: string;
1079
1099
  amount: number;
1080
1100
  side: 'BUY' | 'SELL';
1101
+ price?: number;
1102
+ orderType?: 'FOK' | 'FAK';
1081
1103
  }): Promise<{
1082
1104
  orderId: string;
1083
1105
  success: boolean;
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ import {
43
43
  loadConfig,
44
44
  loadStrategy,
45
45
  writeSampleConfig
46
- } from "./chunk-WTECTX2Z.js";
46
+ } from "./chunk-IVA2SCSN.js";
47
47
  export {
48
48
  AcrossAdapter,
49
49
  AerodromeAdapter,
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@exagent/agent",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
7
10
  "bin": {
8
11
  "exagent": "dist/cli.js"
9
12
  },
@@ -15,29 +18,29 @@
15
18
  },
16
19
  "scripts": {
17
20
  "build": "tsup src/index.ts src/cli.ts --format esm --dts",
18
- "dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch"
21
+ "dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch",
22
+ "test": "tsx --test test/**/*.test.ts"
19
23
  },
20
24
  "dependencies": {
21
25
  "@clack/prompts": "^1.1.0",
22
- "@exagent/sdk": "^0.2.1",
23
- "@polymarket/clob-client": "^4.0.0",
26
+ "@exagent/sdk": "workspace:*",
27
+ "@polymarket/clob-client": "^5.8.1",
24
28
  "boxen": "^8.0.1",
25
- "commander": "^12.0.0",
26
- "ethers": "^5.7.2",
29
+ "commander": "^12.1.0",
27
30
  "figlet": "^1.10.0",
28
31
  "gradient-string": "^3.0.0",
29
32
  "picocolors": "^1.1.1",
30
- "viem": "^2.21.0",
31
- "ws": "^8.16.0",
32
- "zod": "^3.22.0"
33
+ "viem": "^2.48.11",
34
+ "ws": "^8.20.0",
35
+ "zod": "^3.25.76"
33
36
  },
34
37
  "devDependencies": {
35
38
  "@types/figlet": "^1.7.0",
36
39
  "@types/gradient-string": "^1.1.6",
37
- "@types/node": "^20.0.0",
38
- "@types/ws": "^8.5.0",
39
- "tsup": "^8.0.0",
40
- "tsx": "^4.0.0",
41
- "typescript": "^5.6.0"
40
+ "@types/node": "^22.19.18",
41
+ "@types/ws": "^8.18.1",
42
+ "tsup": "^8.5.1",
43
+ "tsx": "^4.21.0",
44
+ "typescript": "^5.9.3"
42
45
  }
43
46
  }
@@ -1,17 +0,0 @@
1
-
2
- > @exagent/agent@0.3.0 build /Users/graydon/Codebase Repertoire/Exagent/packages/agent
3
- > tsup src/index.ts src/cli.ts --format esm --dts
4
-
5
- CLI Building entry: src/cli.ts, src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Target: es2022
9
- ESM Build start
10
- ESM dist/cli.js 9.50 KB
11
- ESM dist/index.js 1.75 KB
12
- ESM dist/chunk-SVFTC5V2.js 201.37 KB
13
- ESM ⚡️ Build success in 34ms
14
- DTS Build start
15
- DTS ⚡️ Build success in 8241ms
16
- DTS dist/cli.d.ts 20.00 B
17
- DTS dist/index.d.ts 41.02 KB
@@ -1,240 +0,0 @@
1
- import { getChainConfig } from '../chains.js';
2
- import { ERC20_ABI } from '../spot/types.js';
3
- import type { SpotDEXClient } from '../spot/client.js';
4
- import {
5
- ACROSS_SPOKE_POOL_ABI,
6
- type BridgeRequest,
7
- type BridgeFeeEstimate,
8
- type BridgeResult,
9
- type BridgeConfig,
10
- } from './types.js';
11
-
12
- const ACROSS_API_BASE = 'https://app.across.to/api';
13
- const ZERO_ADDRESS: `0x${string}` = '0x0000000000000000000000000000000000000000';
14
-
15
- export class AcrossAdapter {
16
- private client: SpotDEXClient;
17
- private config: BridgeConfig;
18
-
19
- constructor(client: SpotDEXClient, config: BridgeConfig) {
20
- this.client = client;
21
- this.config = config;
22
- }
23
-
24
- async estimateFee(request: BridgeRequest): Promise<BridgeFeeEstimate> {
25
- const fromChain = getChainConfig(request.fromChain);
26
- const toChain = getChainConfig(request.toChain);
27
- if (!fromChain) throw new Error(`Unknown source chain: ${request.fromChain}`);
28
- if (!toChain) throw new Error(`Unknown destination chain: ${request.toChain}`);
29
-
30
- // Resolve output token on destination chain (same token, different address)
31
- const outputToken = this.resolveOutputToken(request.token, request.fromChain, request.toChain);
32
-
33
- const params = new URLSearchParams({
34
- inputToken: request.token,
35
- outputToken,
36
- originChainId: fromChain.chainId.toString(),
37
- destinationChainId: toChain.chainId.toString(),
38
- amount: request.amount.toString(),
39
- recipient: request.recipient ?? this.client.address,
40
- });
41
-
42
- const url = `${ACROSS_API_BASE}/suggested-fees?${params}`;
43
- const res = await fetch(url);
44
- if (!res.ok) {
45
- const body = await res.text();
46
- throw new Error(`Across fee API error (${res.status}): ${body}`);
47
- }
48
-
49
- const data = await res.json() as {
50
- totalRelayFee: { pct: string };
51
- relayerCapitalFee: { pct: string };
52
- relayerGasFee: { pct: string };
53
- lpFee: { pct: string };
54
- timestamp: string;
55
- estimatedFillTimeSec: number;
56
- isAmountTooLow: boolean;
57
- limits?: { minDeposit: string };
58
- };
59
-
60
- // Calculate output amount: inputAmount - totalFee
61
- const totalFeePct = parseFloat(data.totalRelayFee.pct) / 1e18;
62
- const feeAmount = BigInt(Math.floor(Number(request.amount) * totalFeePct));
63
- const outputAmount = request.amount - feeAmount;
64
-
65
- return {
66
- totalRelayFeePct: totalFeePct * 100,
67
- capitalFeePct: parseFloat(data.relayerCapitalFee.pct) / 1e18 * 100,
68
- lpFeePct: parseFloat(data.lpFee.pct) / 1e18 * 100,
69
- relayGasFeePct: parseFloat(data.relayerGasFee.pct) / 1e18 * 100,
70
- timestamp: parseInt(data.timestamp),
71
- estimatedFillTimeSec: data.estimatedFillTimeSec,
72
- isAmountTooLow: data.isAmountTooLow,
73
- outputAmount,
74
- };
75
- }
76
-
77
- async deposit(request: BridgeRequest): Promise<BridgeResult> {
78
- const fromChain = getChainConfig(request.fromChain);
79
- const toChain = getChainConfig(request.toChain);
80
- if (!fromChain?.acrossSpokePool) throw new Error(`No Across SpokePool on ${request.fromChain}`);
81
- if (!toChain) throw new Error(`Unknown destination chain: ${request.toChain}`);
82
-
83
- const spokePool = fromChain.acrossSpokePool;
84
- const outputToken = this.resolveOutputToken(request.token, request.fromChain, request.toChain) as `0x${string}`;
85
- const recipient = request.recipient ?? this.client.address;
86
-
87
- // IMPORTANT: Approve BEFORE getting the fee quote.
88
- // The Across SpokePool has a tight depositQuoteTimeBuffer — if there's any
89
- // delay between getting the quoteTimestamp and submitting depositV3, the tx
90
- // reverts with InvalidQuoteTimestamp. Approval can take 10+ seconds on L2s.
91
- await this.client.ensureApproval(request.token, spokePool, request.amount, request.fromChain);
92
-
93
- // Get fee estimate (immediately before submitting — no awaits after this!)
94
- const feeEstimate = await this.estimateFee(request);
95
- if (feeEstimate.isAmountTooLow) {
96
- return {
97
- success: false,
98
- depositTxHash: '0x0' as `0x${string}`,
99
- fromChain: request.fromChain,
100
- toChain: request.toChain,
101
- token: request.token,
102
- amount: request.amount,
103
- fee: 0n,
104
- error: 'Amount too low for Across bridge',
105
- };
106
- }
107
-
108
- const { publicClient, walletClient } = this.client.getClients(request.fromChain);
109
-
110
- // Fill deadline: 6 hours from now
111
- const fillDeadline = Math.floor(Date.now() / 1000) + 21600;
112
-
113
- try {
114
- const hash = await walletClient.writeContract({
115
- address: spokePool,
116
- abi: ACROSS_SPOKE_POOL_ABI,
117
- functionName: 'depositV3',
118
- args: [
119
- this.client.address, // depositor
120
- recipient, // recipient
121
- request.token, // inputToken
122
- outputToken, // outputToken
123
- request.amount, // inputAmount
124
- feeEstimate.outputAmount, // outputAmount (inputAmount - fees)
125
- BigInt(toChain.chainId), // destinationChainId
126
- ZERO_ADDRESS, // exclusiveRelayer (none)
127
- feeEstimate.timestamp, // quoteTimestamp
128
- fillDeadline, // fillDeadline
129
- 0, // exclusivityDeadline (none)
130
- '0x', // message (empty)
131
- ],
132
- });
133
-
134
- const receipt = await publicClient.waitForTransactionReceipt({ hash });
135
- const fee = request.amount - feeEstimate.outputAmount;
136
-
137
- console.log(`[bridge] Across deposit: ${hash} (${request.fromChain} → ${request.toChain})`);
138
-
139
- const result: BridgeResult = {
140
- success: true,
141
- depositTxHash: hash,
142
- fromChain: request.fromChain,
143
- toChain: request.toChain,
144
- token: request.token,
145
- amount: request.amount,
146
- fee,
147
- };
148
-
149
- // Poll for fill
150
- const fill = await this.waitForFill(hash, request.fromChain);
151
- if (fill.filled) {
152
- result.fillTxHash = fill.fillTxHash;
153
- result.fillTimestamp = Date.now();
154
- console.log(`[bridge] Across fill confirmed: ${fill.fillTxHash}`);
155
- } else {
156
- // Deposit tx succeeded but fill was not confirmed within the timeout.
157
- // Mark as failure so the trade signal system doesn't report a completed bridge.
158
- result.success = false;
159
- result.error = 'Bridge deposit submitted but fill not confirmed within timeout. Deposit tx may still be filled — check the Across explorer.';
160
- console.warn(`[bridge] Across fill not confirmed within timeout (deposit: ${hash})`);
161
- }
162
-
163
- return result;
164
- } catch (err) {
165
- return {
166
- success: false,
167
- depositTxHash: '0x0' as `0x${string}`,
168
- fromChain: request.fromChain,
169
- toChain: request.toChain,
170
- token: request.token,
171
- amount: request.amount,
172
- fee: 0n,
173
- error: (err as Error).message,
174
- };
175
- }
176
- }
177
-
178
- async waitForFill(
179
- depositTxHash: string,
180
- originChain: string,
181
- ): Promise<{ filled: boolean; fillTxHash?: `0x${string}` }> {
182
- const chainConfig = getChainConfig(originChain);
183
- if (!chainConfig) return { filled: false };
184
-
185
- const startTime = Date.now();
186
- const timeout = this.config.fillTimeoutMs;
187
- const interval = this.config.pollIntervalMs;
188
-
189
- while (Date.now() - startTime < timeout) {
190
- try {
191
- const params = new URLSearchParams({
192
- depositTxHash,
193
- originChainId: chainConfig.chainId.toString(),
194
- });
195
-
196
- const res = await fetch(`${ACROSS_API_BASE}/deposit/status?${params}`);
197
- if (res.ok) {
198
- const data = await res.json() as {
199
- status: string;
200
- fillTx?: string;
201
- };
202
-
203
- if (data.status === 'filled' && data.fillTx) {
204
- return { filled: true, fillTxHash: data.fillTx as `0x${string}` };
205
- }
206
-
207
- if (data.status === 'expired' || data.status === 'refunded') {
208
- return { filled: false };
209
- }
210
- }
211
- } catch {
212
- // Non-critical — retry on next poll
213
- }
214
-
215
- await new Promise(resolve => setTimeout(resolve, interval));
216
- }
217
-
218
- return { filled: false };
219
- }
220
-
221
- /** Resolve the equivalent token address on the destination chain */
222
- private resolveOutputToken(inputToken: `0x${string}`, fromChain: string, toChain: string): string {
223
- const from = getChainConfig(fromChain);
224
- const to = getChainConfig(toChain);
225
- if (!from || !to) return inputToken;
226
-
227
- // If input is USDC on source chain, output is USDC on destination chain
228
- if (inputToken.toLowerCase() === from.usdcAddress.toLowerCase()) {
229
- return to.usdcAddress;
230
- }
231
-
232
- // If input is WETH on source chain, output is WETH on destination chain
233
- if (inputToken.toLowerCase() === from.wethAddress.toLowerCase()) {
234
- return to.wethAddress;
235
- }
236
-
237
- // Default: same address (for tokens deployed at same address on both chains)
238
- return inputToken;
239
- }
240
- }