@exagent/agent 0.3.5 → 0.3.6
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/dist/{chunk-WTECTX2Z.js → chunk-ZRAOPQQW.js} +208 -147
- package/dist/cli.js +39 -97
- package/dist/index.js +1 -1
- package/package.json +18 -18
- package/src/cli.ts +18 -12
- package/src/config.ts +6 -3
- package/src/llm/anthropic.ts +3 -3
- package/src/llm/deepseek.ts +3 -3
- package/src/llm/google.ts +3 -3
- package/src/llm/groq.ts +3 -3
- package/src/llm/mistral.ts +3 -3
- package/src/llm/ollama.ts +3 -3
- package/src/llm/openai.ts +46 -3
- package/src/llm/together.ts +3 -3
- package/src/llm-providers.ts +8 -100
- package/src/prediction/client.ts +11 -4
- package/src/runtime.ts +3 -3
- package/src/setup.ts +18 -10
- package/src/strategy/loader.ts +136 -62
- package/src/strategy/templates.ts +0 -51
- package/test/strategy-loader.test.ts +150 -0
- package/.turbo/turbo-build.log +0 -17
- package/test-bridge-arb-to-base.mjs +0 -223
- package/test-funded-check.mjs +0 -79
- package/test-funded-phase19.mjs +0 -933
- package/test-hl-deposit-recover.mjs +0 -281
- package/test-hl-withdraw.mjs +0 -372
- package/test-live-signing.mjs +0 -374
- package/test-phase7.mjs +0 -416
- package/test-recover-arb.mjs +0 -206
- package/test-spot-bridge.mjs +0 -248
- package/test-wallet-setup.mjs +0 -126
package/test-phase7.mjs
DELETED
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 7 Integration Tests
|
|
3
|
-
* Tests the actual venue modules against live public APIs + unit tests for local logic.
|
|
4
|
-
* No auth/wallet required for public endpoints.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import assert from 'node:assert';
|
|
8
|
-
|
|
9
|
-
// ═══════════════════════════════════════════════════════════
|
|
10
|
-
// TEST 1: Hyperliquid Public REST API
|
|
11
|
-
// ═══════════════════════════════════════════════════════════
|
|
12
|
-
|
|
13
|
-
async function testHyperliquidClient() {
|
|
14
|
-
console.log('\n═══ TEST 1: Hyperliquid REST Client ═══');
|
|
15
|
-
|
|
16
|
-
const { HyperliquidClient } = await import('./dist/index.js');
|
|
17
|
-
|
|
18
|
-
const client = new HyperliquidClient({
|
|
19
|
-
enabled: true,
|
|
20
|
-
apiUrl: 'https://api.hyperliquid.xyz',
|
|
21
|
-
wsUrl: 'wss://api.hyperliquid.xyz/ws',
|
|
22
|
-
maxLeverage: 10,
|
|
23
|
-
maxNotionalUSD: 50000,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// 1a: getMeta — returns AssetMeta[] (the universe array directly)
|
|
27
|
-
console.log(' [1a] getMeta...');
|
|
28
|
-
const meta = await client.getMeta();
|
|
29
|
-
assert(Array.isArray(meta) && meta.length > 0, 'Meta should be a non-empty array');
|
|
30
|
-
const ethAsset = meta.find(a => a.name === 'ETH');
|
|
31
|
-
assert(ethAsset, 'ETH should exist in meta');
|
|
32
|
-
console.log(` ✅ ${meta.length} assets, ETH szDecimals=${ethAsset.szDecimals}`);
|
|
33
|
-
|
|
34
|
-
// 1b: getAssetIndex
|
|
35
|
-
console.log(' [1b] getAssetIndex...');
|
|
36
|
-
const ethIdx = await client.getAssetIndex('ETH');
|
|
37
|
-
assert(typeof ethIdx === 'number' && ethIdx >= 0, 'ETH index should be a non-negative number');
|
|
38
|
-
const btcIdx = await client.getAssetIndex('BTC');
|
|
39
|
-
assert(typeof btcIdx === 'number' && btcIdx >= 0, 'BTC index should be a non-negative number');
|
|
40
|
-
console.log(` ✅ ETH index=${ethIdx}, BTC index=${btcIdx}`);
|
|
41
|
-
|
|
42
|
-
// 1c: getAllMids — prices (returns Record<string, string>)
|
|
43
|
-
console.log(' [1c] getAllMids...');
|
|
44
|
-
const mids = await client.getAllMids();
|
|
45
|
-
assert(parseFloat(mids['ETH']) > 0, 'ETH mid price should be positive');
|
|
46
|
-
assert(parseFloat(mids['BTC']) > 0, 'BTC mid price should be positive');
|
|
47
|
-
console.log(` ✅ ETH=$${parseFloat(mids['ETH']).toFixed(2)}, BTC=$${parseFloat(mids['BTC']).toFixed(2)}`);
|
|
48
|
-
|
|
49
|
-
// 1d: getMarketData — takes string[] returns PerpMarketData[]
|
|
50
|
-
console.log(' [1d] getMarketData...');
|
|
51
|
-
const marketDataArr = await client.getMarketData(['ETH', 'BTC']);
|
|
52
|
-
assert(Array.isArray(marketDataArr), 'getMarketData should return an array');
|
|
53
|
-
assert(marketDataArr.length === 2, `Should return 2 items, got ${marketDataArr.length}`);
|
|
54
|
-
const ethData = marketDataArr.find(d => d.instrument === 'ETH');
|
|
55
|
-
assert(ethData, 'Should have ETH market data');
|
|
56
|
-
assert(ethData.midPrice > 0, 'ETH midPrice should be positive');
|
|
57
|
-
console.log(` ✅ ETH mid=$${ethData.midPrice.toFixed(2)}, BTC mid=$${marketDataArr.find(d => d.instrument === 'BTC').midPrice.toFixed(2)}`);
|
|
58
|
-
|
|
59
|
-
// 1e: getL2Book
|
|
60
|
-
console.log(' [1e] getL2Book...');
|
|
61
|
-
const book = await client.getL2Book('ETH');
|
|
62
|
-
assert(book.levels.length === 2, 'L2 book should have bids and asks arrays');
|
|
63
|
-
assert(book.levels[0].length > 0, 'Should have bid levels');
|
|
64
|
-
assert(book.levels[1].length > 0, 'Should have ask levels');
|
|
65
|
-
console.log(` ✅ ${book.levels[0].length} bid levels, ${book.levels[1].length} ask levels`);
|
|
66
|
-
|
|
67
|
-
// 1f: invalid asset should throw
|
|
68
|
-
console.log(' [1f] Invalid asset...');
|
|
69
|
-
let threw = false;
|
|
70
|
-
try {
|
|
71
|
-
await client.getAssetIndex('DOESNOTEXIST_XYZ');
|
|
72
|
-
} catch (err) {
|
|
73
|
-
threw = true;
|
|
74
|
-
assert(err.message.includes('Unknown instrument'), `Expected "Unknown instrument" error, got: ${err.message}`);
|
|
75
|
-
}
|
|
76
|
-
assert(threw, 'getAssetIndex should throw for unknown instruments');
|
|
77
|
-
console.log(` ✅ Invalid asset correctly throws "Unknown instrument"`);
|
|
78
|
-
|
|
79
|
-
console.log(' ✅ All Hyperliquid client tests passed');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// ═══════════════════════════════════════════════════════════
|
|
83
|
-
// TEST 2: Polymarket Gamma API (public, no auth)
|
|
84
|
-
// ═══════════════════════════════════════════════════════════
|
|
85
|
-
|
|
86
|
-
async function testPolymarketGammaApi() {
|
|
87
|
-
console.log('\n═══ TEST 2: Polymarket Gamma API ═══');
|
|
88
|
-
|
|
89
|
-
// Test directly since PolymarketClient requires a private key for CLOB init.
|
|
90
|
-
// Gamma API is public — test it raw.
|
|
91
|
-
const GAMMA_URL = 'https://gamma-api.polymarket.com';
|
|
92
|
-
|
|
93
|
-
// 2a: Get active markets
|
|
94
|
-
console.log(' [2a] Get active markets...');
|
|
95
|
-
const resp = await fetch(`${GAMMA_URL}/markets?limit=5&active=true`);
|
|
96
|
-
assert(resp.ok, `Gamma API should return 200, got ${resp.status}`);
|
|
97
|
-
const markets = await resp.json();
|
|
98
|
-
assert(Array.isArray(markets) && markets.length > 0, 'Should return at least one market');
|
|
99
|
-
const first = markets[0];
|
|
100
|
-
assert(first.question, 'Market should have a question');
|
|
101
|
-
assert(first.conditionId || first.condition_id, 'Market should have a conditionId');
|
|
102
|
-
console.log(` ✅ Got ${markets.length} markets, first: "${first.question?.substring(0, 60)}..."`);
|
|
103
|
-
|
|
104
|
-
// 2b: Search markets
|
|
105
|
-
console.log(' [2b] Search markets...');
|
|
106
|
-
const searchResp = await fetch(`${GAMMA_URL}/markets?_q=${encodeURIComponent('bitcoin')}&limit=3&active=true`);
|
|
107
|
-
assert(searchResp.ok, 'Search should succeed');
|
|
108
|
-
const searchResults = await searchResp.json();
|
|
109
|
-
assert(Array.isArray(searchResults), 'Search should return array');
|
|
110
|
-
console.log(` ✅ Search "bitcoin" returned ${searchResults.length} results`);
|
|
111
|
-
|
|
112
|
-
// 2c: Trending markets (by volume)
|
|
113
|
-
console.log(' [2c] Trending markets...');
|
|
114
|
-
const trendingResp = await fetch(`${GAMMA_URL}/markets?active=true&limit=3&order=volume24hr&ascending=false`);
|
|
115
|
-
assert(trendingResp.ok, 'Trending should succeed');
|
|
116
|
-
const trending = await trendingResp.json();
|
|
117
|
-
assert(Array.isArray(trending) && trending.length > 0, 'Should have trending markets');
|
|
118
|
-
console.log(` ✅ Top trending: "${trending[0]?.question?.substring(0, 60)}..."`);
|
|
119
|
-
|
|
120
|
-
console.log(' ✅ All Polymarket Gamma API tests passed');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ═══════════════════════════════════════════════════════════
|
|
124
|
-
// TEST 3: Prediction instrument encoding/decoding
|
|
125
|
-
// ═══════════════════════════════════════════════════════════
|
|
126
|
-
|
|
127
|
-
async function testPredictionTypes() {
|
|
128
|
-
console.log('\n═══ TEST 3: Prediction Instrument Encoding ═══');
|
|
129
|
-
|
|
130
|
-
// Dynamic import doesn't re-export the functions from types directly,
|
|
131
|
-
// so we import from the source path
|
|
132
|
-
const { encodePredictionInstrument } = await import('./dist/index.js');
|
|
133
|
-
|
|
134
|
-
// We need decodePredictionInstrument too but it may not be exported.
|
|
135
|
-
// Let's test what we can.
|
|
136
|
-
|
|
137
|
-
// 3a: Encode
|
|
138
|
-
console.log(' [3a] encodePredictionInstrument...');
|
|
139
|
-
const encoded = encodePredictionInstrument('0xabc123def', 0);
|
|
140
|
-
assert(encoded === 'POLY:0xabc123def:0', `Expected POLY:0xabc123def:0, got ${encoded}`);
|
|
141
|
-
const encoded2 = encodePredictionInstrument('0xfoo', 1);
|
|
142
|
-
assert(encoded2 === 'POLY:0xfoo:1', `Expected POLY:0xfoo:1, got ${encoded2}`);
|
|
143
|
-
console.log(` ✅ Encoding works: ${encoded}, ${encoded2}`);
|
|
144
|
-
|
|
145
|
-
console.log(' ✅ Prediction types tests passed');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ═══════════════════════════════════════════════════════════
|
|
149
|
-
// TEST 4: Position Tracker — short positions & venue scoping
|
|
150
|
-
// ═══════════════════════════════════════════════════════════
|
|
151
|
-
|
|
152
|
-
async function testPositionTracker() {
|
|
153
|
-
console.log('\n═══ TEST 4: Position Tracker (Short Positions) ═══');
|
|
154
|
-
|
|
155
|
-
const { PositionTracker, FileStore } = await import('./dist/index.js');
|
|
156
|
-
|
|
157
|
-
// Use a temp store
|
|
158
|
-
const store = new FileStore('/tmp/exagent-test-positions.json');
|
|
159
|
-
const tracker = new PositionTracker(store);
|
|
160
|
-
tracker.reset();
|
|
161
|
-
|
|
162
|
-
// 4a: Basic spot buy/sell
|
|
163
|
-
console.log(' [4a] Spot buy/sell...');
|
|
164
|
-
tracker.recordBuy('ETH', 1.0, 3000, 3.0, 'uniswap', 'ethereum');
|
|
165
|
-
let positions = tracker.getPositions();
|
|
166
|
-
assert(positions.length === 1, 'Should have 1 position');
|
|
167
|
-
assert(positions[0].token === 'ETH', 'Token should be ETH');
|
|
168
|
-
assert(positions[0].quantity === 1.0, 'Quantity should be 1.0');
|
|
169
|
-
assert(positions[0].costBasisPerUnit === 3000, 'Cost basis should be 3000');
|
|
170
|
-
|
|
171
|
-
tracker.recordSell('ETH', 1.0, 3500, 3.5, 'uniswap', 'ethereum');
|
|
172
|
-
positions = tracker.getPositions();
|
|
173
|
-
assert(positions.length === 0, 'Position should be closed');
|
|
174
|
-
const realizedPnl = tracker.getRealizedPnL();
|
|
175
|
-
// PnL = (3500 - 3000) * 1.0 - 3.5 = 496.5
|
|
176
|
-
assert(Math.abs(realizedPnl - 496.5) < 0.01, `Expected ~496.5 realized PnL, got ${realizedPnl}`);
|
|
177
|
-
console.log(` ✅ Spot PnL correct: $${realizedPnl.toFixed(2)}`);
|
|
178
|
-
|
|
179
|
-
// 4b: Perp short position
|
|
180
|
-
console.log(' [4b] Perp short open/close...');
|
|
181
|
-
tracker.reset();
|
|
182
|
-
tracker.recordSell('BTC', 0.1, 95000, 4.75, 'hyperliquid_perp');
|
|
183
|
-
positions = tracker.getPositions();
|
|
184
|
-
assert(positions.length === 1, 'Should have 1 short position');
|
|
185
|
-
assert(positions[0].quantity === -0.1, `Short quantity should be -0.1, got ${positions[0].quantity}`);
|
|
186
|
-
assert(positions[0].costBasisPerUnit === 95000, 'Entry should be 95000');
|
|
187
|
-
|
|
188
|
-
// Close at profit (buy back lower)
|
|
189
|
-
tracker.recordBuy('BTC', 0.1, 90000, 4.50, 'hyperliquid_perp');
|
|
190
|
-
positions = tracker.getPositions();
|
|
191
|
-
assert(positions.length === 0, 'Short should be closed');
|
|
192
|
-
const shortPnl = tracker.getRealizedPnL();
|
|
193
|
-
// PnL = (95000 - 90000) * 0.1 - 4.50 = 495.5
|
|
194
|
-
assert(Math.abs(shortPnl - 495.5) < 0.01, `Expected ~495.5 short PnL, got ${shortPnl}`);
|
|
195
|
-
console.log(` ✅ Short PnL correct: $${shortPnl.toFixed(2)}`);
|
|
196
|
-
|
|
197
|
-
// 4c: Perp short at loss
|
|
198
|
-
console.log(' [4c] Perp short at loss...');
|
|
199
|
-
tracker.reset();
|
|
200
|
-
tracker.recordSell('SOL', 50, 145, 0.72, 'hyperliquid_perp');
|
|
201
|
-
tracker.recordBuy('SOL', 50, 160, 0.80, 'hyperliquid_perp');
|
|
202
|
-
const lossShortPnl = tracker.getRealizedPnL();
|
|
203
|
-
// PnL = (145 - 160) * 50 - 0.80 = -750.80
|
|
204
|
-
assert(Math.abs(lossShortPnl - (-750.80)) < 0.01, `Expected ~-750.80, got ${lossShortPnl}`);
|
|
205
|
-
console.log(` ✅ Short loss correct: $${lossShortPnl.toFixed(2)}`);
|
|
206
|
-
|
|
207
|
-
// 4d: Venue-scoped positions (same token, different venues)
|
|
208
|
-
console.log(' [4d] Venue-scoped positions...');
|
|
209
|
-
tracker.reset();
|
|
210
|
-
tracker.recordBuy('ETH', 1.0, 3000, 3.0, 'uniswap', 'ethereum');
|
|
211
|
-
tracker.recordBuy('ETH', 2.0, 3100, 6.2, 'hyperliquid_perp');
|
|
212
|
-
positions = tracker.getPositions();
|
|
213
|
-
assert(positions.length === 2, `Should have 2 separate positions, got ${positions.length}`);
|
|
214
|
-
const uniPos = positions.find(p => p.venue === 'uniswap');
|
|
215
|
-
const hlPos = positions.find(p => p.venue === 'hyperliquid_perp');
|
|
216
|
-
assert(uniPos && uniPos.quantity === 1.0, 'Uniswap ETH should be 1.0');
|
|
217
|
-
assert(hlPos && hlPos.quantity === 2.0, 'HL ETH should be 2.0');
|
|
218
|
-
console.log(` ✅ Same token on different venues tracked separately`);
|
|
219
|
-
|
|
220
|
-
// 4e: Unrealized PnL direction — long vs short
|
|
221
|
-
console.log(' [4e] Unrealized PnL direction...');
|
|
222
|
-
tracker.reset();
|
|
223
|
-
tracker.recordBuy('ETH', 1.0, 3000, 0, 'hyperliquid_perp'); // Long at 3000
|
|
224
|
-
tracker.recordSell('BTC', 0.1, 95000, 0, 'hyperliquid_perp'); // Short at 95000
|
|
225
|
-
|
|
226
|
-
const summary = tracker.getSummary({ ETH: 3500, BTC: 90000 });
|
|
227
|
-
// Long ETH: (3500 - 3000) * 1.0 = +500
|
|
228
|
-
// Short BTC: (95000 - 90000) * 0.1 = +500
|
|
229
|
-
assert(Math.abs(summary.totalUnrealizedPnL - 1000) < 0.01,
|
|
230
|
-
`Expected ~1000 unrealized PnL (both profitable), got ${summary.totalUnrealizedPnL}`);
|
|
231
|
-
console.log(` ✅ Unrealized PnL: $${summary.totalUnrealizedPnL.toFixed(2)} (long+short both green)`);
|
|
232
|
-
|
|
233
|
-
// Price moves against both
|
|
234
|
-
const badSummary = tracker.getSummary({ ETH: 2500, BTC: 100000 });
|
|
235
|
-
// Long ETH: (2500 - 3000) * 1.0 = -500
|
|
236
|
-
// Short BTC: (95000 - 100000) * 0.1 = -500
|
|
237
|
-
assert(Math.abs(badSummary.totalUnrealizedPnL - (-1000)) < 0.01,
|
|
238
|
-
`Expected ~-1000 unrealized PnL (both underwater), got ${badSummary.totalUnrealizedPnL}`);
|
|
239
|
-
console.log(` ✅ Unrealized PnL: $${badSummary.totalUnrealizedPnL.toFixed(2)} (both red)`);
|
|
240
|
-
|
|
241
|
-
// 4f: Adding to short position
|
|
242
|
-
console.log(' [4f] Adding to short position...');
|
|
243
|
-
tracker.reset();
|
|
244
|
-
tracker.recordSell('DOGE', 50000, 0.185, 4.63, 'hyperliquid_perp');
|
|
245
|
-
tracker.recordSell('DOGE', 50000, 0.195, 4.88, 'hyperliquid_perp');
|
|
246
|
-
positions = tracker.getPositions();
|
|
247
|
-
assert(positions.length === 1, 'Should be one aggregated short');
|
|
248
|
-
assert(positions[0].quantity === -100000, `Should be -100000, got ${positions[0].quantity}`);
|
|
249
|
-
// Avg entry: (0.185*50000 + 0.195*50000) / 100000 = 0.19
|
|
250
|
-
assert(Math.abs(positions[0].costBasisPerUnit - 0.19) < 0.001,
|
|
251
|
-
`Avg entry should be ~0.19, got ${positions[0].costBasisPerUnit}`);
|
|
252
|
-
console.log(` ✅ Aggregated short: ${positions[0].quantity} @ $${positions[0].costBasisPerUnit}`);
|
|
253
|
-
|
|
254
|
-
// 4g: Partial close of short
|
|
255
|
-
console.log(' [4g] Partial close of short...');
|
|
256
|
-
tracker.recordBuy('DOGE', 60000, 0.17, 5.10, 'hyperliquid_perp');
|
|
257
|
-
positions = tracker.getPositions();
|
|
258
|
-
assert(positions.length === 1, 'Should still have position');
|
|
259
|
-
assert(positions[0].quantity === -40000, `Should be -40000, got ${positions[0].quantity}`);
|
|
260
|
-
const partialPnl = tracker.getRealizedPnL();
|
|
261
|
-
// PnL = (0.19 - 0.17) * 60000 - 5.10 = 1194.90
|
|
262
|
-
assert(Math.abs(partialPnl - 1194.90) < 0.01, `Expected ~1194.90, got ${partialPnl}`);
|
|
263
|
-
console.log(` ✅ Partial close PnL: $${partialPnl.toFixed(2)}, remaining: ${positions[0].quantity}`);
|
|
264
|
-
|
|
265
|
-
console.log(' ✅ All position tracker tests passed');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// ═══════════════════════════════════════════════════════════
|
|
269
|
-
// TEST 5: Config Schema Validation
|
|
270
|
-
// ═══════════════════════════════════════════════════════════
|
|
271
|
-
|
|
272
|
-
async function testConfigSchema() {
|
|
273
|
-
console.log('\n═══ TEST 5: Config Schema Validation ═══');
|
|
274
|
-
|
|
275
|
-
const { loadConfig, generateSampleConfig } = await import('./dist/index.js');
|
|
276
|
-
|
|
277
|
-
// 5a: Generate sample config (returns JSON string)
|
|
278
|
-
console.log(' [5a] generateSampleConfig...');
|
|
279
|
-
const sampleStr = generateSampleConfig('test-agent', 'http://localhost:3002');
|
|
280
|
-
assert(typeof sampleStr === 'string', 'generateSampleConfig should return a string');
|
|
281
|
-
const sample = JSON.parse(sampleStr);
|
|
282
|
-
assert(sample.agentId === 'test-agent', 'Agent ID should match');
|
|
283
|
-
assert(sample.venues, 'Should have venues section');
|
|
284
|
-
assert(sample.venues.hyperliquid_perp, 'Should have HL perp config');
|
|
285
|
-
assert(sample.venues.polymarket, 'Should have PM config');
|
|
286
|
-
assert(sample.venues.hyperliquid_perp.enabled === false, 'HL should be disabled by default');
|
|
287
|
-
assert(sample.venues.polymarket.enabled === false, 'PM should be disabled by default');
|
|
288
|
-
console.log(` ✅ Sample config has venues section, both disabled by default`);
|
|
289
|
-
|
|
290
|
-
// 5b: Validate config with venues
|
|
291
|
-
console.log(' [5b] Config with wallet and venues...');
|
|
292
|
-
const configStr = JSON.stringify({
|
|
293
|
-
...sample,
|
|
294
|
-
apiToken: 'test-token',
|
|
295
|
-
wallet: { privateKey: '0x' + 'a'.repeat(64) },
|
|
296
|
-
venues: {
|
|
297
|
-
hyperliquid_perp: {
|
|
298
|
-
enabled: true,
|
|
299
|
-
apiUrl: 'https://api.hyperliquid.xyz',
|
|
300
|
-
wsUrl: 'wss://api.hyperliquid.xyz/ws',
|
|
301
|
-
maxLeverage: 20,
|
|
302
|
-
maxNotionalUSD: 100000,
|
|
303
|
-
},
|
|
304
|
-
polymarket: {
|
|
305
|
-
enabled: true,
|
|
306
|
-
clobApiUrl: 'https://clob.polymarket.com',
|
|
307
|
-
gammaApiUrl: 'https://gamma-api.polymarket.com',
|
|
308
|
-
maxNotionalUSD: 5000,
|
|
309
|
-
maxTotalExposureUSD: 20000,
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// Write temp config and load it
|
|
315
|
-
const fs = await import('node:fs');
|
|
316
|
-
const tmpPath = '/tmp/exagent-test-config.json';
|
|
317
|
-
fs.writeFileSync(tmpPath, configStr);
|
|
318
|
-
const loaded = loadConfig(tmpPath);
|
|
319
|
-
assert(loaded.wallet?.privateKey === '0x' + 'a'.repeat(64), 'Wallet private key should load');
|
|
320
|
-
assert(loaded.venues?.hyperliquid_perp?.enabled === true, 'HL should be enabled');
|
|
321
|
-
assert(loaded.venues?.hyperliquid_perp?.maxLeverage === 20, 'HL max leverage should be 20');
|
|
322
|
-
assert(loaded.venues?.polymarket?.enabled === true, 'PM should be enabled');
|
|
323
|
-
assert(loaded.venues?.polymarket?.maxTotalExposureUSD === 20000, 'PM max exposure should be 20000');
|
|
324
|
-
console.log(` ✅ Config loads and validates with wallet + venues`);
|
|
325
|
-
|
|
326
|
-
// 5c: Config without wallet/venues (backward compat)
|
|
327
|
-
console.log(' [5c] Config without wallet (backward compat)...');
|
|
328
|
-
const minimalObj = { ...sample };
|
|
329
|
-
delete minimalObj.wallet;
|
|
330
|
-
delete minimalObj.venues;
|
|
331
|
-
minimalObj.apiToken = 'test-token';
|
|
332
|
-
const minimalStr = JSON.stringify(minimalObj);
|
|
333
|
-
fs.writeFileSync(tmpPath, minimalStr);
|
|
334
|
-
const minimal = loadConfig(tmpPath);
|
|
335
|
-
assert(!minimal.wallet, 'Wallet should be undefined when not in config');
|
|
336
|
-
assert(!minimal.venues, 'Venues should be undefined when not in config');
|
|
337
|
-
console.log(` ✅ Minimal config (no wallet/venues) loads fine`);
|
|
338
|
-
|
|
339
|
-
// 5d: EXAGENT_WALLET_PRIVATE_KEY env var override
|
|
340
|
-
console.log(' [5d] Env var override...');
|
|
341
|
-
process.env.EXAGENT_WALLET_PRIVATE_KEY = '0x' + 'b'.repeat(64);
|
|
342
|
-
fs.writeFileSync(tmpPath, minimalStr);
|
|
343
|
-
const envLoaded = loadConfig(tmpPath);
|
|
344
|
-
assert(envLoaded.wallet?.privateKey === '0x' + 'b'.repeat(64), 'Env var should override wallet');
|
|
345
|
-
delete process.env.EXAGENT_WALLET_PRIVATE_KEY;
|
|
346
|
-
console.log(` ✅ EXAGENT_WALLET_PRIVATE_KEY env var works`);
|
|
347
|
-
|
|
348
|
-
// Cleanup
|
|
349
|
-
fs.unlinkSync(tmpPath);
|
|
350
|
-
|
|
351
|
-
console.log(' ✅ All config tests passed');
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ═══════════════════════════════════════════════════════════
|
|
355
|
-
// TEST 6: Hyperliquid Signer — nonce generation
|
|
356
|
-
// ═══════════════════════════════════════════════════════════
|
|
357
|
-
|
|
358
|
-
async function testSignerNonces() {
|
|
359
|
-
console.log('\n═══ TEST 6: Signer Nonce Generation ═══');
|
|
360
|
-
|
|
361
|
-
const { getNextNonce, HYPERLIQUID_DOMAIN } = await import('./dist/index.js');
|
|
362
|
-
|
|
363
|
-
// 6a: Nonces should be strictly increasing
|
|
364
|
-
console.log(' [6a] Strictly increasing nonces...');
|
|
365
|
-
const nonces = [];
|
|
366
|
-
for (let i = 0; i < 10; i++) {
|
|
367
|
-
nonces.push(getNextNonce());
|
|
368
|
-
}
|
|
369
|
-
for (let i = 1; i < nonces.length; i++) {
|
|
370
|
-
assert(nonces[i] > nonces[i - 1], `Nonce ${i} (${nonces[i]}) should be > nonce ${i-1} (${nonces[i-1]})`);
|
|
371
|
-
}
|
|
372
|
-
console.log(` ✅ 10 nonces all strictly increasing: ${nonces[0]} → ${nonces[nonces.length - 1]}`);
|
|
373
|
-
|
|
374
|
-
// 6b: Domain should have correct chainId
|
|
375
|
-
console.log(' [6b] Domain chainId...');
|
|
376
|
-
assert(HYPERLIQUID_DOMAIN.chainId === 42161 || HYPERLIQUID_DOMAIN.chainId === 42161n,
|
|
377
|
-
`Chain ID should be 42161 (Arbitrum), got ${HYPERLIQUID_DOMAIN.chainId}`);
|
|
378
|
-
assert(HYPERLIQUID_DOMAIN.name === 'HyperliquidSignTransaction',
|
|
379
|
-
`Domain name should be HyperliquidSignTransaction, got ${HYPERLIQUID_DOMAIN.name}`);
|
|
380
|
-
console.log(` ✅ Domain: ${HYPERLIQUID_DOMAIN.name}, chainId=${HYPERLIQUID_DOMAIN.chainId}`);
|
|
381
|
-
|
|
382
|
-
console.log(' ✅ All signer tests passed');
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// ═══════════════════════════════════════════════════════════
|
|
386
|
-
// RUN ALL TESTS
|
|
387
|
-
// ═══════════════════════════════════════════════════════════
|
|
388
|
-
|
|
389
|
-
let passed = 0;
|
|
390
|
-
let failed = 0;
|
|
391
|
-
|
|
392
|
-
const tests = [
|
|
393
|
-
['Hyperliquid Client', testHyperliquidClient],
|
|
394
|
-
['Polymarket Gamma API', testPolymarketGammaApi],
|
|
395
|
-
['Prediction Types', testPredictionTypes],
|
|
396
|
-
['Position Tracker', testPositionTracker],
|
|
397
|
-
['Config Schema', testConfigSchema],
|
|
398
|
-
['Signer Nonces', testSignerNonces],
|
|
399
|
-
];
|
|
400
|
-
|
|
401
|
-
for (const [name, fn] of tests) {
|
|
402
|
-
try {
|
|
403
|
-
await fn();
|
|
404
|
-
passed++;
|
|
405
|
-
} catch (err) {
|
|
406
|
-
failed++;
|
|
407
|
-
console.error(`\n ❌ ${name} FAILED: ${err.message}`);
|
|
408
|
-
console.error(` ${err.stack?.split('\n')[1]?.trim()}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
console.log(`\n${'═'.repeat(50)}`);
|
|
413
|
-
console.log(`Results: ${passed} passed, ${failed} failed, ${tests.length} total`);
|
|
414
|
-
console.log(`${'═'.repeat(50)}`);
|
|
415
|
-
|
|
416
|
-
if (failed > 0) process.exit(1);
|
package/test-recover-arb.mjs
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Recovery script: bridge remaining USDC from Arbitrum → Base via Across.
|
|
3
|
-
* Then return all Base funds to deployer.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
createPublicClient,
|
|
8
|
-
createWalletClient,
|
|
9
|
-
http,
|
|
10
|
-
formatEther,
|
|
11
|
-
formatUnits,
|
|
12
|
-
parseEther,
|
|
13
|
-
maxUint256,
|
|
14
|
-
} from 'viem';
|
|
15
|
-
import { privateKeyToAccount } from 'viem/accounts';
|
|
16
|
-
import { base, arbitrum } from 'viem/chains';
|
|
17
|
-
|
|
18
|
-
const DEPLOYER_KEY = '0x0991f4e17be491bb11f4cf1d079db1771fe669e2b0d735f2b3ffc0e32d230ac9';
|
|
19
|
-
const TEST_KEY = '0xb027d931f6c8b4b2681451716981432130806f01ff87001c74412b504b810dbe';
|
|
20
|
-
|
|
21
|
-
const deployer = privateKeyToAccount(DEPLOYER_KEY);
|
|
22
|
-
const testWallet = privateKeyToAccount(TEST_KEY);
|
|
23
|
-
|
|
24
|
-
const basePublic = createPublicClient({ chain: base, transport: http('https://mainnet.base.org') });
|
|
25
|
-
const baseTestWallet = createWalletClient({ account: testWallet, chain: base, transport: http('https://mainnet.base.org') });
|
|
26
|
-
const arbPublic = createPublicClient({ chain: arbitrum, transport: http('https://arb1.arbitrum.io/rpc') });
|
|
27
|
-
const arbTestWallet = createWalletClient({ account: testWallet, chain: arbitrum, transport: http('https://arb1.arbitrum.io/rpc') });
|
|
28
|
-
|
|
29
|
-
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
30
|
-
const USDC_ARB = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831';
|
|
31
|
-
const ACROSS_SPOKE_ARB = '0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A';
|
|
32
|
-
|
|
33
|
-
const ERC20_ABI = [
|
|
34
|
-
{ type: 'function', name: 'approve', inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ type: 'bool' }], stateMutability: 'nonpayable' },
|
|
35
|
-
{ type: 'function', name: 'transfer', inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ type: 'bool' }], stateMutability: 'nonpayable' },
|
|
36
|
-
{ type: 'function', name: 'balanceOf', inputs: [{ name: 'account', type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
|
|
37
|
-
{ type: 'function', name: 'allowance', inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
const ACROSS_SPOKE_ABI = [
|
|
41
|
-
{
|
|
42
|
-
type: 'function', name: 'depositV3',
|
|
43
|
-
inputs: [
|
|
44
|
-
{ name: 'depositor', type: 'address' },
|
|
45
|
-
{ name: 'recipient', type: 'address' },
|
|
46
|
-
{ name: 'inputToken', type: 'address' },
|
|
47
|
-
{ name: 'outputToken', type: 'address' },
|
|
48
|
-
{ name: 'inputAmount', type: 'uint256' },
|
|
49
|
-
{ name: 'outputAmount', type: 'uint256' },
|
|
50
|
-
{ name: 'destinationChainId', type: 'uint256' },
|
|
51
|
-
{ name: 'exclusiveRelayer', type: 'address' },
|
|
52
|
-
{ name: 'quoteTimestamp', type: 'uint32' },
|
|
53
|
-
{ name: 'fillDeadline', type: 'uint32' },
|
|
54
|
-
{ name: 'exclusivityDeadline', type: 'uint32' },
|
|
55
|
-
{ name: 'message', type: 'bytes' },
|
|
56
|
-
],
|
|
57
|
-
outputs: [],
|
|
58
|
-
stateMutability: 'payable',
|
|
59
|
-
},
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
63
|
-
|
|
64
|
-
async function waitForTx(client, hash, label) {
|
|
65
|
-
console.log(` ⏳ ${label}: tx ${hash.slice(0, 10)}...`);
|
|
66
|
-
const receipt = await client.waitForTransactionReceipt({ hash, timeout: 120_000 });
|
|
67
|
-
if (receipt.status === 'reverted') throw new Error(`${label} REVERTED: ${hash}`);
|
|
68
|
-
console.log(` ✅ ${label}: confirmed (gas: ${receipt.gasUsed})`);
|
|
69
|
-
return receipt;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Check current state ──
|
|
73
|
-
console.log('Current balances:');
|
|
74
|
-
const arbUsdc = await arbPublic.readContract({ address: USDC_ARB, abi: ERC20_ABI, functionName: 'balanceOf', args: [testWallet.address] });
|
|
75
|
-
const arbEth = await arbPublic.getBalance({ address: testWallet.address });
|
|
76
|
-
console.log(` Arb: ${formatUnits(arbUsdc, 6)} USDC, ${formatEther(arbEth)} ETH`);
|
|
77
|
-
|
|
78
|
-
const baseUsdc = await basePublic.readContract({ address: USDC_BASE, abi: ERC20_ABI, functionName: 'balanceOf', args: [testWallet.address] });
|
|
79
|
-
const baseEth = await basePublic.getBalance({ address: testWallet.address });
|
|
80
|
-
console.log(` Base: ${formatUnits(baseUsdc, 6)} USDC, ${formatEther(baseEth)} ETH`);
|
|
81
|
-
|
|
82
|
-
if (arbUsdc === 0n) {
|
|
83
|
-
console.log('No USDC on Arbitrum to bridge back.');
|
|
84
|
-
process.exit(0);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ── Bridge USDC Arb → Base ──
|
|
88
|
-
console.log(`\nBridging ${formatUnits(arbUsdc, 6)} USDC from Arbitrum to Base...`);
|
|
89
|
-
|
|
90
|
-
// Get fresh fee quote IMMEDIATELY before submitting tx (prevents stale timestamp)
|
|
91
|
-
const params = new URLSearchParams({
|
|
92
|
-
inputToken: USDC_ARB,
|
|
93
|
-
outputToken: USDC_BASE,
|
|
94
|
-
originChainId: '42161',
|
|
95
|
-
destinationChainId: '8453',
|
|
96
|
-
amount: arbUsdc.toString(),
|
|
97
|
-
recipient: testWallet.address,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const feeRes = await fetch(`https://app.across.to/api/suggested-fees?${params}`);
|
|
101
|
-
if (!feeRes.ok) {
|
|
102
|
-
console.log(`Fee API error: ${feeRes.status}`);
|
|
103
|
-
console.log(await feeRes.text());
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const feeData = await feeRes.json();
|
|
108
|
-
console.log(` isAmountTooLow: ${feeData.isAmountTooLow}`);
|
|
109
|
-
|
|
110
|
-
if (feeData.isAmountTooLow) {
|
|
111
|
-
console.log('Amount too low for Across bridge. Sending USDC to deployer on Arbitrum instead.');
|
|
112
|
-
// Just send to deployer on Arb — they'll have USDC there for future use
|
|
113
|
-
const hash = await arbTestWallet.writeContract({
|
|
114
|
-
address: USDC_ARB, abi: ERC20_ABI, functionName: 'transfer',
|
|
115
|
-
args: [deployer.address, arbUsdc],
|
|
116
|
-
});
|
|
117
|
-
await waitForTx(arbPublic, hash, 'Send USDC to deployer on Arb');
|
|
118
|
-
console.log('Done. Deployer has USDC on Arbitrum for future HL deposits.');
|
|
119
|
-
process.exit(0);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const outputAmount = BigInt(arbUsdc) - BigInt(feeData.totalRelayFee.total);
|
|
123
|
-
const timestamp = parseInt(feeData.timestamp);
|
|
124
|
-
const fillDeadline = Math.floor(Date.now() / 1000) + 21600;
|
|
125
|
-
|
|
126
|
-
console.log(` Fee: $${(Number(feeData.totalRelayFee.total) / 1e6).toFixed(4)}`);
|
|
127
|
-
console.log(` Output: ${formatUnits(outputAmount, 6)} USDC on Base`);
|
|
128
|
-
|
|
129
|
-
// Check/set approval (already approved from previous attempt)
|
|
130
|
-
const allowance = await arbPublic.readContract({
|
|
131
|
-
address: USDC_ARB, abi: ERC20_ABI, functionName: 'allowance',
|
|
132
|
-
args: [testWallet.address, ACROSS_SPOKE_ARB],
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (allowance < arbUsdc) {
|
|
136
|
-
const appHash = await arbTestWallet.writeContract({
|
|
137
|
-
address: USDC_ARB, abi: ERC20_ABI, functionName: 'approve',
|
|
138
|
-
args: [ACROSS_SPOKE_ARB, maxUint256],
|
|
139
|
-
});
|
|
140
|
-
await waitForTx(arbPublic, appHash, 'Approval');
|
|
141
|
-
} else {
|
|
142
|
-
console.log(' Already approved');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Submit bridge tx IMMEDIATELY after getting quote (no delay)
|
|
146
|
-
console.log(' Submitting depositV3...');
|
|
147
|
-
const hash = await arbTestWallet.writeContract({
|
|
148
|
-
address: ACROSS_SPOKE_ARB,
|
|
149
|
-
abi: ACROSS_SPOKE_ABI,
|
|
150
|
-
functionName: 'depositV3',
|
|
151
|
-
args: [
|
|
152
|
-
testWallet.address, testWallet.address,
|
|
153
|
-
USDC_ARB, USDC_BASE,
|
|
154
|
-
arbUsdc, outputAmount,
|
|
155
|
-
8453n,
|
|
156
|
-
'0x0000000000000000000000000000000000000000',
|
|
157
|
-
timestamp, fillDeadline, 0, '0x',
|
|
158
|
-
],
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
await waitForTx(arbPublic, hash, 'Arb→Base bridge');
|
|
162
|
-
|
|
163
|
-
// Poll for fill
|
|
164
|
-
console.log(' Polling for fill...');
|
|
165
|
-
for (let i = 0; i < 60; i++) {
|
|
166
|
-
await sleep(2000);
|
|
167
|
-
try {
|
|
168
|
-
const statusRes = await fetch(`https://app.across.to/api/deposit/status?depositTxHash=${hash}&originChainId=42161`);
|
|
169
|
-
if (statusRes.ok) {
|
|
170
|
-
const status = await statusRes.json();
|
|
171
|
-
if (status.status === 'filled') {
|
|
172
|
-
console.log(' ✅ Bridge filled!');
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
if (i % 5 === 0) console.log(` ... ${status.status} (${i*2}s)`);
|
|
176
|
-
}
|
|
177
|
-
} catch {}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
await sleep(3000);
|
|
181
|
-
|
|
182
|
-
// ── Return Base funds to deployer ──
|
|
183
|
-
console.log('\nReturning funds to deployer on Base...');
|
|
184
|
-
|
|
185
|
-
const finalBaseUsdc = await basePublic.readContract({ address: USDC_BASE, abi: ERC20_ABI, functionName: 'balanceOf', args: [testWallet.address] });
|
|
186
|
-
if (finalBaseUsdc > 0n) {
|
|
187
|
-
const h = await baseTestWallet.writeContract({
|
|
188
|
-
address: USDC_BASE, abi: ERC20_ABI, functionName: 'transfer',
|
|
189
|
-
args: [deployer.address, finalBaseUsdc],
|
|
190
|
-
});
|
|
191
|
-
await waitForTx(basePublic, h, 'Return USDC');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const finalBaseEth = await basePublic.getBalance({ address: testWallet.address });
|
|
195
|
-
if (finalBaseEth > 200000000000000n) { // > 0.0002 ETH
|
|
196
|
-
const returnEth = finalBaseEth - 100000000000000n; // keep 0.0001
|
|
197
|
-
const h = await baseTestWallet.sendTransaction({ to: deployer.address, value: returnEth });
|
|
198
|
-
await waitForTx(basePublic, h, 'Return ETH');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ── Final report ──
|
|
202
|
-
console.log('\n═══ Final Deployer Balance ═══');
|
|
203
|
-
const dEth = await basePublic.getBalance({ address: deployer.address });
|
|
204
|
-
const dUsdc = await basePublic.readContract({ address: USDC_BASE, abi: ERC20_ABI, functionName: 'balanceOf', args: [deployer.address] });
|
|
205
|
-
console.log(` Base: ${formatEther(dEth)} ETH, ${formatUnits(dUsdc, 6)} USDC`);
|
|
206
|
-
console.log('\nDone!');
|