@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.
@@ -0,0 +1,150 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { loadStrategy } from '../src/strategy/loader.js';
7
+ import type { StrategyContext } from '@exagent/sdk';
8
+
9
+ function createContext(llmContent: string, prices: Record<string, number> = { ETH: 3000 }): StrategyContext {
10
+ const logs: string[] = [];
11
+ return {
12
+ llm: {
13
+ async chat() {
14
+ return { content: llmContent };
15
+ },
16
+ getMetadata() {
17
+ return { provider: 'test', model: 'test' };
18
+ },
19
+ },
20
+ market: {
21
+ getPrice(symbol: string) {
22
+ return prices[symbol] ?? prices[symbol.toUpperCase()];
23
+ },
24
+ getPrices() {
25
+ return prices;
26
+ },
27
+ getOHLCV() {
28
+ return [];
29
+ },
30
+ },
31
+ position: {
32
+ openPositions: [],
33
+ totalUnrealizedPnL: 0,
34
+ totalRealizedPnL: 0,
35
+ },
36
+ store: {
37
+ get() {
38
+ return undefined;
39
+ },
40
+ set() {
41
+ // no-op
42
+ },
43
+ delete() {
44
+ // no-op
45
+ },
46
+ keys() {
47
+ return [];
48
+ },
49
+ },
50
+ config: {
51
+ mode: 'paper',
52
+ timeHorizon: 'swing',
53
+ maxPositionSizeBps: 2000,
54
+ maxDailyLossBps: 500,
55
+ maxConcurrentPositions: 5,
56
+ tradingIntervalMs: 60000,
57
+ maxSlippageBps: 100,
58
+ minTradeValueUSD: 10,
59
+ initialCapitalUSD: 10000,
60
+ },
61
+ log(message: string) {
62
+ logs.push(message);
63
+ },
64
+ };
65
+ }
66
+
67
+ test('loadStrategy rejects raw JavaScript code configs', async () => {
68
+ await assert.rejects(
69
+ () => loadStrategy({ code: 'globalThis.stoleSecrets = true; return [];' }),
70
+ /Raw JavaScript strategy code is disabled/,
71
+ );
72
+ });
73
+
74
+ test('template strategies run through prompt-backed validation', async () => {
75
+ const strategy = await loadStrategy({ template: 'momentum' });
76
+ const signals = await strategy(createContext(JSON.stringify([
77
+ { symbol: 'ETH', side: 'buy', venue: 'hyperliquid_perp', confidence: 0.8 },
78
+ ])));
79
+
80
+ assert.equal(signals.length, 1);
81
+ assert.equal(signals[0].symbol, 'ETH');
82
+ assert.equal(signals[0].venue, 'hyperliquid_perp');
83
+ assert.equal(signals[0].price, 3000);
84
+ assert.equal(signals[0].venueFillId, '');
85
+ });
86
+
87
+ test('prompt strategies drop venues outside the configured strategy scope', async () => {
88
+ const strategy = await loadStrategy({
89
+ prompt: {
90
+ name: 'Scoped Prompt',
91
+ systemPrompt: 'Return scoped trade intents.',
92
+ venues: ['hyperliquid_perp'],
93
+ },
94
+ });
95
+
96
+ const signals = await strategy(createContext(JSON.stringify([
97
+ { symbol: 'ETH', side: 'buy', venue: 'polymarket', confidence: 0.9 },
98
+ { symbol: 'ETH', side: 'buy', venue: 'hyperliquid_perp', confidence: 0.9 },
99
+ ])));
100
+
101
+ assert.equal(signals.length, 1);
102
+ assert.equal(signals[0].venue, 'hyperliquid_perp');
103
+ });
104
+
105
+ test('strategy metadata files are parsed as data, not executable modules', async () => {
106
+ const dir = mkdtempSync(join(tmpdir(), 'exagent-strategy-'));
107
+ const strategyPath = join(dir, 'strategy.ts');
108
+
109
+ writeFileSync(strategyPath, [
110
+ '// Generated by Exagent',
111
+ 'export const name = "Prompt File";',
112
+ 'export const venues = ["hyperliquid_perp"];',
113
+ 'export const systemPrompt = "Return trade intents.";'
114
+ ].join('\n'));
115
+
116
+ try {
117
+ const strategy = await loadStrategy({ file: strategyPath });
118
+ const signals = await strategy(createContext(JSON.stringify([
119
+ { symbol: 'ETH', side: 'long', confidence: 0.7 },
120
+ ])));
121
+
122
+ assert.equal(signals.length, 1);
123
+ assert.equal(signals[0].venue, 'hyperliquid_perp');
124
+ } finally {
125
+ rmSync(dir, { recursive: true, force: true });
126
+ }
127
+ });
128
+
129
+ test('strategy files cannot export code or arbitrary statements', async () => {
130
+ const dir = mkdtempSync(join(tmpdir(), 'exagent-strategy-'));
131
+ const codeExportPath = join(dir, 'code-strategy.ts');
132
+ const statementPath = join(dir, 'statement-strategy.ts');
133
+
134
+ writeFileSync(codeExportPath, 'export const code = "return [];";\n');
135
+ writeFileSync(statementPath, 'export const systemPrompt = "Return []";\nglobalThis.compromised = true;\n');
136
+
137
+ try {
138
+ await assert.rejects(
139
+ () => loadStrategy({ file: codeExportPath }),
140
+ /Raw JavaScript strategy code is disabled/,
141
+ );
142
+ await assert.rejects(
143
+ () => loadStrategy({ file: statementPath }),
144
+ /Unsupported statement/,
145
+ );
146
+ assert.equal((globalThis as Record<string, unknown>).compromised, undefined);
147
+ } finally {
148
+ rmSync(dir, { recursive: true, force: true });
149
+ }
150
+ });
@@ -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,223 +0,0 @@
1
- /**
2
- * Fix the Arb → Base bridge issue (InvalidQuoteTimestamp).
3
- *
4
- * Root cause: The Across SpokePool's depositQuoteTimeBuffer is tight.
5
- * If there's any delay between getting the fee quote and submitting the tx,
6
- * the quoteTimestamp becomes stale and the tx reverts.
7
- *
8
- * Fix: Do approval BEFORE getting the quote, then submit IMMEDIATELY after.
9
- */
10
-
11
- import {
12
- createPublicClient,
13
- createWalletClient,
14
- http,
15
- formatUnits,
16
- formatEther,
17
- parseEther,
18
- maxUint256,
19
- } from 'viem';
20
- import { privateKeyToAccount } from 'viem/accounts';
21
- import { base, arbitrum } from 'viem/chains';
22
-
23
- const DEPLOYER_KEY = '0x0991f4e17be491bb11f4cf1d079db1771fe669e2b0d735f2b3ffc0e32d230ac9';
24
- const TEST_KEY = '0xb027d931f6c8b4b2681451716981432130806f01ff87001c74412b504b810dbe';
25
-
26
- const deployer = privateKeyToAccount(DEPLOYER_KEY);
27
- const testWallet = privateKeyToAccount(TEST_KEY);
28
-
29
- const arbPublic = createPublicClient({ chain: arbitrum, transport: http('https://arb1.arbitrum.io/rpc') });
30
- const arbDeployerWallet = createWalletClient({ account: deployer, chain: arbitrum, transport: http('https://arb1.arbitrum.io/rpc') });
31
- const arbTestWallet = createWalletClient({ account: testWallet, chain: arbitrum, transport: http('https://arb1.arbitrum.io/rpc') });
32
-
33
- const basePublic = createPublicClient({ chain: base, transport: http('https://mainnet.base.org') });
34
- const baseTestWallet = createWalletClient({ account: testWallet, chain: base, transport: http('https://mainnet.base.org') });
35
-
36
- const USDC_ARB = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831';
37
- const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
38
- const ACROSS_SPOKE_ARB = '0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A';
39
-
40
- const ERC20_ABI = [
41
- { type: 'function', name: 'transfer', inputs: [{name:'to',type:'address'},{name:'amount',type:'uint256'}], outputs: [{type:'bool'}], stateMutability: 'nonpayable' },
42
- { type: 'function', name: 'balanceOf', inputs: [{name:'account',type:'address'}], outputs: [{type:'uint256'}], stateMutability: 'view' },
43
- { type: 'function', name: 'approve', inputs: [{name:'spender',type:'address'},{name:'amount',type:'uint256'}], outputs: [{type:'bool'}], stateMutability: 'nonpayable' },
44
- { type: 'function', name: 'allowance', inputs: [{name:'owner',type:'address'},{name:'spender',type:'address'}], outputs: [{type:'uint256'}], stateMutability: 'view' },
45
- ];
46
-
47
- const ACROSS_SPOKE_ABI = [
48
- {
49
- type: 'function', name: 'depositV3',
50
- inputs: [
51
- { name: 'depositor', type: 'address' }, { name: 'recipient', type: 'address' },
52
- { name: 'inputToken', type: 'address' }, { name: 'outputToken', type: 'address' },
53
- { name: 'inputAmount', type: 'uint256' }, { name: 'outputAmount', type: 'uint256' },
54
- { name: 'destinationChainId', type: 'uint256' },
55
- { name: 'exclusiveRelayer', type: 'address' },
56
- { name: 'quoteTimestamp', type: 'uint32' }, { name: 'fillDeadline', type: 'uint32' },
57
- { name: 'exclusivityDeadline', type: 'uint32' }, { name: 'message', type: 'bytes' },
58
- ],
59
- outputs: [],
60
- stateMutability: 'payable',
61
- },
62
- ];
63
-
64
- function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
65
-
66
- async function waitForTx(client, hash, label) {
67
- console.log(` ⏳ ${label}: tx ${hash.slice(0, 10)}...`);
68
- const receipt = await client.waitForTransactionReceipt({ hash, timeout: 120_000 });
69
- if (receipt.status === 'reverted') throw new Error(`${label} REVERTED: ${hash}`);
70
- console.log(` ✅ ${label}: confirmed (gas: ${receipt.gasUsed})`);
71
- return receipt;
72
- }
73
-
74
- // ═══ Step 1: Fund deployer with ETH on Arb (from test wallet) ═══
75
- console.log('═══ Step 1: Give deployer ETH on Arb for gas ═══');
76
-
77
- const deployerArbEth = await arbPublic.getBalance({ address: deployer.address });
78
- if (deployerArbEth < parseEther('0.0003')) {
79
- console.log(' Sending 0.0005 ETH from test wallet to deployer on Arb...');
80
- const h = await arbTestWallet.sendTransaction({
81
- to: deployer.address,
82
- value: parseEther('0.0005'),
83
- });
84
- await waitForTx(arbPublic, h, 'ETH to deployer');
85
- }
86
-
87
- // ═══ Step 2: Deployer sends USDC to test wallet ═══
88
- console.log('\n═══ Step 2: Deployer sends USDC to test wallet on Arb ═══');
89
-
90
- const deployerUsdc = await arbPublic.readContract({ address: USDC_ARB, abi: ERC20_ABI, functionName: 'balanceOf', args: [deployer.address] });
91
- console.log(` Deployer USDC: ${formatUnits(deployerUsdc, 6)}`);
92
-
93
- if (deployerUsdc > 0n) {
94
- const h = await arbDeployerWallet.writeContract({
95
- address: USDC_ARB, abi: ERC20_ABI, functionName: 'transfer',
96
- args: [testWallet.address, deployerUsdc],
97
- });
98
- await waitForTx(arbPublic, h, 'USDC to test wallet');
99
- }
100
-
101
- const testUsdc = await arbPublic.readContract({ address: USDC_ARB, abi: ERC20_ABI, functionName: 'balanceOf', args: [testWallet.address] });
102
- console.log(` Test wallet USDC: ${formatUnits(testUsdc, 6)}`);
103
-
104
- // ═══ Step 3: Pre-approve USDC to Across SpokePool (BEFORE getting quote) ═══
105
- console.log('\n═══ Step 3: Pre-approve USDC to Across ═══');
106
-
107
- const allowance = await arbPublic.readContract({
108
- address: USDC_ARB, abi: ERC20_ABI, functionName: 'allowance',
109
- args: [testWallet.address, ACROSS_SPOKE_ARB],
110
- });
111
-
112
- if (allowance < testUsdc) {
113
- const h = await arbTestWallet.writeContract({
114
- address: USDC_ARB, abi: ERC20_ABI, functionName: 'approve',
115
- args: [ACROSS_SPOKE_ARB, maxUint256],
116
- });
117
- await waitForTx(arbPublic, h, 'Approve Across');
118
- } else {
119
- console.log(' Already approved');
120
- }
121
-
122
- // ═══ Step 4: Get quote + submit bridge IMMEDIATELY (no delays!) ═══
123
- console.log('\n═══ Step 4: Bridge USDC Arb → Base (quote + submit together) ═══');
124
-
125
- const bridgeAmount = testUsdc;
126
- console.log(` Bridging ${formatUnits(bridgeAmount, 6)} USDC...`);
127
-
128
- // Get fee quote
129
- const params = new URLSearchParams({
130
- inputToken: USDC_ARB,
131
- outputToken: USDC_BASE,
132
- originChainId: '42161',
133
- destinationChainId: '8453',
134
- amount: bridgeAmount.toString(),
135
- recipient: testWallet.address,
136
- });
137
-
138
- const feeRes = await fetch(`https://app.across.to/api/suggested-fees?${params}`);
139
- if (!feeRes.ok) {
140
- console.log(` ❌ Fee API error: ${await feeRes.text()}`);
141
- process.exit(1);
142
- }
143
-
144
- const feeData = await feeRes.json();
145
-
146
- if (feeData.isAmountTooLow) {
147
- console.log(' ❌ Amount too low for bridge');
148
- process.exit(1);
149
- }
150
-
151
- const outputAmount = BigInt(bridgeAmount) - BigInt(feeData.totalRelayFee.total);
152
- const quoteTimestamp = parseInt(feeData.timestamp);
153
- const fillDeadline = Math.floor(Date.now() / 1000) + 21600;
154
-
155
- console.log(` Quote timestamp: ${quoteTimestamp}`);
156
- console.log(` Output: ${formatUnits(outputAmount, 6)} USDC`);
157
-
158
- // IMMEDIATELY submit — no awaits between quote and this call!
159
- console.log(' Submitting depositV3 IMMEDIATELY...');
160
- const bridgeHash = await arbTestWallet.writeContract({
161
- address: ACROSS_SPOKE_ARB,
162
- abi: ACROSS_SPOKE_ABI,
163
- functionName: 'depositV3',
164
- args: [
165
- testWallet.address, testWallet.address,
166
- USDC_ARB, USDC_BASE,
167
- bridgeAmount, outputAmount,
168
- 8453n,
169
- '0x0000000000000000000000000000000000000000',
170
- quoteTimestamp, fillDeadline, 0, '0x',
171
- ],
172
- });
173
-
174
- await waitForTx(arbPublic, bridgeHash, 'Arb→Base bridge');
175
-
176
- // Poll for fill
177
- console.log(' Polling for fill...');
178
- for (let i = 0; i < 60; i++) {
179
- await sleep(2000);
180
- try {
181
- const sRes = await fetch(`https://app.across.to/api/deposit/status?depositTxHash=${bridgeHash}&originChainId=42161`);
182
- if (sRes.ok) {
183
- const s = await sRes.json();
184
- if (s.status === 'filled') { console.log(' ✅ Bridge filled!'); break; }
185
- if (i % 5 === 0) console.log(` ... ${s.status} (${i*2}s)`);
186
- }
187
- } catch {}
188
- }
189
-
190
- await sleep(3000);
191
-
192
- // ═══ Step 5: Return all funds to deployer on Base ═══
193
- console.log('\n═══ Step 5: Return funds to deployer ═══');
194
-
195
- const baseUsdc = await basePublic.readContract({ address: USDC_BASE, abi: ERC20_ABI, functionName: 'balanceOf', args: [testWallet.address] });
196
- if (baseUsdc > 0n) {
197
- console.log(` Returning ${formatUnits(baseUsdc, 6)} USDC...`);
198
- const h = await baseTestWallet.writeContract({
199
- address: USDC_BASE, abi: ERC20_ABI, functionName: 'transfer',
200
- args: [deployer.address, baseUsdc],
201
- });
202
- await waitForTx(basePublic, h, 'Return USDC');
203
- }
204
-
205
- // Return remaining Base ETH
206
- const baseEth = await basePublic.getBalance({ address: testWallet.address });
207
- if (baseEth > 200000000000000n) {
208
- const returnEth = baseEth - 100000000000000n;
209
- const h = await baseTestWallet.sendTransaction({ to: deployer.address, value: returnEth });
210
- await waitForTx(basePublic, h, 'Return ETH');
211
- }
212
-
213
- // Final report
214
- console.log('\n═══ Final Deployer Balance ═══');
215
- const dEth = await basePublic.getBalance({ address: deployer.address });
216
- const dUsdc = await basePublic.readContract({ address: USDC_BASE, abi: ERC20_ABI, functionName: 'balanceOf', args: [deployer.address] });
217
- console.log(` Base: ${formatEther(dEth)} ETH, ${formatUnits(dUsdc, 6)} USDC`);
218
-
219
- const dArbUsdc = await arbPublic.readContract({ address: USDC_ARB, abi: ERC20_ABI, functionName: 'balanceOf', args: [deployer.address] });
220
- const dArbEth = await arbPublic.getBalance({ address: deployer.address });
221
- console.log(` Arb: ${formatEther(dArbEth)} ETH, ${formatUnits(dArbUsdc, 6)} USDC`);
222
-
223
- console.log('\nDone!');
@@ -1,79 +0,0 @@
1
- /**
2
- * Check deployer and test wallet balances before funded tests.
3
- * Read-only — no transactions.
4
- */
5
-
6
- import { createPublicClient, http, formatEther, formatUnits } from 'viem';
7
- import { base, arbitrum, polygon } from 'viem/chains';
8
-
9
- const DEPLOYER = '0x1374Ee64A05011877660fD53A0D757B65Cbc73ce';
10
- const TEST_WALLET_1 = '0xf5C450448E531162141e69c70F9D8cfA637604F4';
11
-
12
- const ERC20_ABI = [
13
- { type: 'function', name: 'balanceOf', inputs: [{ name: 'account', type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
14
- { type: 'function', name: 'decimals', inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' },
15
- { type: 'function', name: 'symbol', inputs: [], outputs: [{ type: 'string' }], stateMutability: 'view' },
16
- ];
17
-
18
- const TOKENS = {
19
- base: {
20
- USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
21
- USDbC: '0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA',
22
- },
23
- arbitrum: {
24
- USDC: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
25
- 'USDC.e': '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8',
26
- },
27
- polygon: {
28
- USDC: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
29
- 'USDC.e': '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
30
- },
31
- };
32
-
33
- const clients = {
34
- base: createPublicClient({ chain: base, transport: http('https://mainnet.base.org') }),
35
- arbitrum: createPublicClient({ chain: arbitrum, transport: http('https://arb1.arbitrum.io/rpc') }),
36
- polygon: createPublicClient({ chain: polygon, transport: http('https://polygon-bor-rpc.publicnode.com') }),
37
- };
38
-
39
- async function checkWallet(name, address) {
40
- console.log(`\n─── ${name}: ${address} ───`);
41
-
42
- for (const [chainName, client] of Object.entries(clients)) {
43
- try {
44
- const ethBal = await client.getBalance({ address });
45
- const ethStr = formatEther(ethBal);
46
- const nativeName = chainName === 'polygon' ? 'POL' : 'ETH';
47
-
48
- const tokens = TOKENS[chainName];
49
- let tokenLine = '';
50
- for (const [symbol, tokenAddr] of Object.entries(tokens)) {
51
- try {
52
- const bal = await client.readContract({ address: tokenAddr, abi: ERC20_ABI, functionName: 'balanceOf', args: [address] });
53
- const decimals = await client.readContract({ address: tokenAddr, abi: ERC20_ABI, functionName: 'decimals' });
54
- const formatted = formatUnits(bal, decimals);
55
- if (parseFloat(formatted) > 0) {
56
- tokenLine += ` | ${formatted} ${symbol}`;
57
- }
58
- } catch {
59
- // token might not exist on chain
60
- }
61
- }
62
-
63
- if (parseFloat(ethStr) > 0 || tokenLine) {
64
- console.log(` ${chainName}: ${ethStr} ${nativeName}${tokenLine}`);
65
- }
66
- } catch (err) {
67
- console.log(` ${chainName}: RPC error (${err.shortMessage || err.message})`);
68
- }
69
- }
70
- }
71
-
72
- console.log('Wallet Balance Check');
73
- console.log('====================');
74
-
75
- await checkWallet('Deployer', DEPLOYER);
76
- await checkWallet('Test Wallet 1 (Phase 7)', TEST_WALLET_1);
77
-
78
- console.log('\n====================');
79
- console.log('Done. Use these balances to plan funded tests.');