openbroker 1.0.80 → 1.0.82

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.
@@ -42,6 +42,20 @@ function loadEnvFile(): string | null {
42
42
  const verbose = process.env.VERBOSE === '1' || process.env.VERBOSE === 'true';
43
43
  process.env.DOTENV_CONFIG_QUIET = 'true';
44
44
 
45
+ // Explicit config path via -c / --config (highest priority — overrides shell env vars)
46
+ const explicitPath = process.env.OPENBROKER_CONFIG;
47
+ if (explicitPath) {
48
+ if (existsSync(explicitPath)) {
49
+ loadDotenv({ path: explicitPath, override: true });
50
+ if (verbose) {
51
+ console.log(`[DEBUG] Loaded config from -c flag: ${explicitPath}`);
52
+ }
53
+ return explicitPath;
54
+ }
55
+ console.error(`Config file not found: ${explicitPath}`);
56
+ process.exit(1);
57
+ }
58
+
45
59
  // Check locations in order of priority
46
60
  const locations = [
47
61
  { path: LOCAL_ENV_PATH, name: 'local (.env)' },
@@ -124,6 +138,10 @@ export function loadConfig(): OpenBrokerConfig {
124
138
  // Determine if this is an API wallet setup
125
139
  const isApiWallet = accountAddress !== undefined && accountAddress !== walletAddress;
126
140
 
141
+ // Vault address — only for vault trading (ERC4626 contracts / native vaults via CoreWriter).
142
+ // Standard API wallets (approved via approveAgent) do NOT need this.
143
+ const vaultAddress = process.env.HYPERLIQUID_VAULT_ADDRESS?.toLowerCase();
144
+
127
145
  return {
128
146
  baseUrl,
129
147
  privateKey: privateKey as `0x${string}`,
@@ -134,6 +152,7 @@ export function loadConfig(): OpenBrokerConfig {
134
152
  builderAddress,
135
153
  builderFee,
136
154
  slippageBps,
155
+ vaultAddress,
137
156
  };
138
157
  }
139
158
 
@@ -12,6 +12,7 @@ export interface OpenBrokerConfig {
12
12
  builderAddress: string;
13
13
  builderFee: number; // tenths of bps (10 = 1 bps)
14
14
  slippageBps: number;
15
+ vaultAddress?: string; // Explicit vault address for vault trading (ERC4626 / native vaults via CoreWriter)
15
16
  }
16
17
 
17
18
  // ============ Builder ============
@@ -155,8 +155,11 @@ export function generateCloid(): string {
155
155
  * Returns true if approved, false if not
156
156
  */
157
157
  export async function checkBuilderFeeApproval(
158
- client: { getMaxBuilderFee: () => Promise<string | null>; builderAddress: string }
158
+ client: { getMaxBuilderFee: () => Promise<string | null>; builderAddress: string; isTestnet: boolean }
159
159
  ): Promise<boolean> {
160
+ // Skip builder fee check on testnet — builder may not have balance
161
+ if (client.isTestnet) return true;
162
+
160
163
  const approval = await client.getMaxBuilderFee();
161
164
  if (!approval) {
162
165
  console.log('⚠️ Builder fee not approved!');
@@ -123,15 +123,19 @@ async function main() {
123
123
  console.log(`Account Mode: ${modeLabel[accountMode] ?? accountMode}`);
124
124
 
125
125
  if (!isOtherAccount) {
126
- // Check builder fee approval
127
- const builderApproval = await client.getMaxBuilderFee();
128
- console.log(`Builder Address: ${client.builderAddress}`);
129
- console.log(`Builder Fee: ${client.builderFeeBps} bps`);
130
- if (builderApproval) {
131
- console.log(`Builder Approved: ✅ Yes (max: ${builderApproval})`);
126
+ // Check builder fee approval (skip on testnet)
127
+ if (client.isTestnet) {
128
+ console.log(`Builder Fee: skipped (testnet)`);
132
129
  } else {
133
- console.log(`Builder Approved: No`);
134
- console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
130
+ const builderApproval = await client.getMaxBuilderFee();
131
+ console.log(`Builder Address: ${client.builderAddress}`);
132
+ console.log(`Builder Fee: ${client.builderFeeBps} bps`);
133
+ if (builderApproval) {
134
+ console.log(`Builder Approved: ✅ Yes (max: ${builderApproval})`);
135
+ } else {
136
+ console.log(`Builder Approved: ❌ No`);
137
+ console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
138
+ }
135
139
  }
136
140
 
137
141
  // Warn if API wallet setup looks misconfigured
@@ -108,6 +108,9 @@ async function main() {
108
108
 
109
109
  // Fetch HIP-3 perps
110
110
  if (args.type === 'all' || args.type === 'hip3') {
111
+ if (client.isTestnet) {
112
+ console.log('Note: Testnet — HIP-3 dexes not auto-loaded. Use "dexName:COIN" syntax to load a specific dex.\n');
113
+ }
111
114
  try {
112
115
  const allPerpMetas = await client.getAllPerpMetas();
113
116
  // Skip index 0 (main dex), process HIP-3 dexs
@@ -67,6 +67,9 @@ async function main() {
67
67
  try {
68
68
  // Load metadata (needed for HIP-3 coin resolution)
69
69
  await client.getMetaAndAssetCtxs();
70
+ if (client.isTestnet && coin.includes(':')) {
71
+ await client.loadSingleHip3Dex(coin.split(':')[0]);
72
+ }
70
73
 
71
74
  const now = Date.now();
72
75
  const startTime = now - (bars * (INTERVAL_MS[interval] || 3_600_000));
@@ -43,6 +43,9 @@ async function main() {
43
43
  try {
44
44
  // Load metadata (needed for HIP-3 coin resolution)
45
45
  await client.getMetaAndAssetCtxs();
46
+ if (client.isTestnet && coin.includes(':')) {
47
+ await client.loadSingleHip3Dex(coin.split(':')[0]);
48
+ }
46
49
 
47
50
  const now = Date.now();
48
51
  const startTime = now - (hours * 3_600_000);
@@ -184,6 +184,10 @@ async function main() {
184
184
  console.log('==================================\n');
185
185
  console.log(`Threshold: ${threshold}% annualized | Scope: ${mainOnly ? 'main only' : hip3Only ? 'HIP-3 only' : 'all dexes'}\n`);
186
186
 
187
+ if (client.isTestnet && !mainOnly) {
188
+ console.log('Note: Testnet — HIP-3 dexes not auto-loaded. Only main perps will be scanned.\n');
189
+ }
190
+
187
191
  const options = { threshold, mainOnly, hip3Only, topN };
188
192
 
189
193
  const runScan = async () => {
@@ -56,7 +56,14 @@ async function main() {
56
56
  }
57
57
 
58
58
  // Include HIP-3 dex assets
59
+ if (client.isTestnet && (includeHip3 || showAll) && !(filterCoin?.includes(':'))) {
60
+ console.log('Note: Testnet — HIP-3 dexes not auto-loaded. Use --coin dexName:COIN to view a specific HIP-3 asset.\n');
61
+ }
59
62
  if (includeHip3 || showAll || (filterCoin && filterCoin.includes(':'))) {
63
+ // On testnet, load the specific dex on demand if user specified dex:COIN
64
+ if (client.isTestnet && filterCoin?.includes(':')) {
65
+ await client.loadSingleHip3Dex(filterCoin.split(':')[0]);
66
+ }
60
67
  const allPerps = await client.getAllPerpMetas();
61
68
  for (const dexData of allPerps) {
62
69
  if (!dexData.dexName) continue; // Skip main dex (already loaded)
@@ -59,7 +59,14 @@ async function main() {
59
59
  }
60
60
 
61
61
  // Include HIP-3 markets
62
+ if (client.isTestnet && includeHip3 && !(filterCoin?.includes(':'))) {
63
+ console.log('Note: Testnet — HIP-3 dexes not auto-loaded. Use --coin dexName:COIN to view a specific HIP-3 asset.\n');
64
+ }
62
65
  if (includeHip3 || (filterCoin && filterCoin.includes(':'))) {
66
+ // On testnet, load the specific dex on demand if user specified dex:COIN
67
+ if (client.isTestnet && filterCoin?.includes(':')) {
68
+ await client.loadSingleHip3Dex(filterCoin.split(':')[0]);
69
+ }
63
70
  const allPerps = await client.getAllPerpMetas();
64
71
  for (const dexData of allPerps) {
65
72
  if (!dexData.dexName) continue;
@@ -45,6 +45,11 @@ async function main() {
45
45
  const lookupAddress = targetAddress?.toLowerCase();
46
46
 
47
47
  try {
48
+ // On testnet, load specific HIP-3 dex on demand if filtering by dex:COIN
49
+ if (client.isTestnet && filterCoin?.includes(':')) {
50
+ await client.loadSingleHip3Dex(filterCoin.split(':')[0]);
51
+ }
52
+
48
53
  if (openOnly) {
49
54
  // Use the dedicated open orders endpoint
50
55
  let openOrders = await client.getOpenOrders(lookupAddress);
@@ -122,6 +122,14 @@ async function main() {
122
122
 
123
123
  // Search HIP-3 perps
124
124
  if (args.type === 'all' || args.type === 'hip3') {
125
+ if (client.isTestnet) {
126
+ // On testnet, load specific dex if query is "dex:COIN" format
127
+ if (args.query.includes(':')) {
128
+ await client.loadSingleHip3Dex(args.query.split(':')[0]);
129
+ } else {
130
+ console.log(' (Testnet: HIP-3 dexes not auto-loaded. Use "dexName:COIN" to search a specific dex.)\n');
131
+ }
132
+ }
125
133
  try {
126
134
  const allPerpMetas = await client.getAllPerpMetas();
127
135
  // Skip index 0 (main dex), process HIP-3 dexs
@@ -43,6 +43,9 @@ async function main() {
43
43
  try {
44
44
  // Load metadata (needed for HIP-3 coin resolution)
45
45
  await client.getMetaAndAssetCtxs();
46
+ if (client.isTestnet && coin.includes(':')) {
47
+ await client.loadSingleHip3Dex(coin.split(':')[0]);
48
+ }
46
49
 
47
50
  let trades = await client.getRecentTrades(normalizeCoin(coin));
48
51
 
@@ -12,8 +12,19 @@ const OPEN_BROKER_BUILDER_ADDRESS = '0xbb67021fA3e62ab4DA985bb5a55c5c1884381068'
12
12
  const OPENBROKER_URL = process.env.OPENBROKER_URL || 'https://openbroker.dev';
13
13
 
14
14
  // Global config directory: ~/.openbroker/
15
- const CONFIG_DIR = path.join(homedir(), '.openbroker');
16
- const CONFIG_PATH = path.join(CONFIG_DIR, '.env');
15
+ const GLOBAL_CONFIG_DIR = path.join(homedir(), '.openbroker');
16
+ const GLOBAL_CONFIG_PATH = path.join(GLOBAL_CONFIG_DIR, '.env');
17
+
18
+ // Parse CLI flags
19
+ const cliArgs = process.argv.slice(2);
20
+ const useTestnet = cliArgs.includes('--testnet') || process.env.HYPERLIQUID_NETWORK === 'testnet';
21
+ const accountAddressIdx = cliArgs.indexOf('--account-address');
22
+ const cliAccountAddress = accountAddressIdx !== -1 ? cliArgs[accountAddressIdx + 1] : undefined;
23
+ const configPathIdx = cliArgs.indexOf('-c') !== -1 ? cliArgs.indexOf('-c') : cliArgs.indexOf('--config');
24
+ const cliConfigPath = configPathIdx !== -1 ? cliArgs[configPathIdx + 1] : process.env.OPENBROKER_CONFIG;
25
+
26
+ const CONFIG_PATH = cliConfigPath ? path.resolve(cliConfigPath) : GLOBAL_CONFIG_PATH;
27
+ const CONFIG_DIR = path.dirname(CONFIG_PATH);
17
28
 
18
29
  interface OnboardResult {
19
30
  success: boolean;
@@ -54,7 +65,7 @@ const POLL_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
54
65
 
55
66
  async function pollForApproval(agentAddress: string): Promise<string | null> {
56
67
  const startTime = Date.now();
57
- const statusUrl = `${OPENBROKER_URL}/api/approve-status?agent=${agentAddress}`;
68
+ const statusUrl = `${OPENBROKER_URL}/api/approve-status?agent=${agentAddress}${useTestnet ? '&network=testnet' : ''}`;
58
69
 
59
70
  let dotCount = 0;
60
71
 
@@ -86,8 +97,9 @@ async function pollForApproval(agentAddress: string): Promise<string | null> {
86
97
  }
87
98
 
88
99
  async function verifyBuilderFee(masterAddress: string): Promise<boolean> {
100
+ const apiUrl = useTestnet ? 'https://api.hyperliquid-testnet.xyz/info' : 'https://api.hyperliquid.xyz/info';
89
101
  try {
90
- const response = await fetch('https://api.hyperliquid.xyz/info', {
102
+ const response = await fetch(apiUrl, {
91
103
  method: 'POST',
92
104
  headers: { 'Content-Type': 'application/json' },
93
105
  body: JSON.stringify({
@@ -104,8 +116,9 @@ async function verifyBuilderFee(masterAddress: string): Promise<boolean> {
104
116
  }
105
117
 
106
118
  function buildApiWalletEnvContent(privateKey: string, masterAddress: string): string {
119
+ const network = useTestnet ? 'testnet' : 'mainnet';
107
120
  return `# OpenBroker Configuration (API Wallet)
108
- # Location: ~/.openbroker/.env
121
+ # Location: ${CONFIG_PATH}
109
122
  # WARNING: Keep this file secret! Never share it!
110
123
 
111
124
  # API wallet private key (can trade, cannot withdraw)
@@ -115,7 +128,7 @@ HYPERLIQUID_PRIVATE_KEY=${privateKey}
115
128
  HYPERLIQUID_ACCOUNT_ADDRESS=${masterAddress}
116
129
 
117
130
  # Network: mainnet or testnet
118
- HYPERLIQUID_NETWORK=mainnet
131
+ HYPERLIQUID_NETWORK=${network}
119
132
  `;
120
133
  }
121
134
 
@@ -132,7 +145,7 @@ async function setupApiWallet(): Promise<OnboardResult> {
132
145
  ensureConfigDir();
133
146
 
134
147
  // Build the approval URL
135
- const approveUrl = `${OPENBROKER_URL}/approve?agent=${apiAccount.address}`;
148
+ const approveUrl = `${OPENBROKER_URL}/approve?agent=${apiAccount.address}${useTestnet ? '&network=testnet' : ''}`;
136
149
 
137
150
  console.log(`✅ Config directory ready: ${CONFIG_DIR}\n`);
138
151
 
@@ -141,9 +154,15 @@ async function setupApiWallet(): Promise<OnboardResult> {
141
154
  console.log('Your API wallet needs to be authorized by a master wallet.');
142
155
  console.log('Open this URL in your browser and connect your master wallet:\n');
143
156
  console.log(` ${approveUrl}\n`);
144
- console.log('The master wallet will sign two transactions:');
145
- console.log(' 1. ApproveAgent — authorizes this API wallet to trade');
146
- console.log(' 2. ApproveBuilderFee approves the 1 bps builder fee\n');
157
+ if (useTestnet) {
158
+ console.log('The master wallet will sign one transaction:');
159
+ console.log(' 1. ApproveAgent authorizes this API wallet to trade');
160
+ console.log(' (Builder fee approval is skipped on testnet)\n');
161
+ } else {
162
+ console.log('The master wallet will sign two transactions:');
163
+ console.log(' 1. ApproveAgent — authorizes this API wallet to trade');
164
+ console.log(' 2. ApproveBuilderFee — approves the 1 bps builder fee\n');
165
+ }
147
166
 
148
167
  // Poll for approval
149
168
  const masterAddress = await pollForApproval(apiAccount.address);
@@ -175,14 +194,18 @@ HYPERLIQUID_NETWORK=mainnet
175
194
 
176
195
  console.log(`\n✅ Master wallet detected: ${masterAddress}`);
177
196
 
178
- // Verify on-chain
179
- console.log(' Verifying builder fee approval...');
180
- const feeApproved = await verifyBuilderFee(masterAddress);
197
+ // Verify builder fee on-chain (skip on testnet)
198
+ if (!useTestnet) {
199
+ console.log(' Verifying builder fee approval...');
200
+ const feeApproved = await verifyBuilderFee(masterAddress);
181
201
 
182
- if (feeApproved) {
183
- console.log(' ✅ Builder fee: approved on-chain');
202
+ if (feeApproved) {
203
+ console.log(' ✅ Builder fee: approved on-chain');
204
+ } else {
205
+ console.log(' ⚠️ Builder fee not yet confirmed on-chain (may take a moment)');
206
+ }
184
207
  } else {
185
- console.log(' ⚠️ Builder fee not yet confirmed on-chain (may take a moment)');
208
+ console.log(' (Builder fee verification skipped on testnet)');
186
209
  }
187
210
 
188
211
  // Save complete config
@@ -225,8 +248,30 @@ HYPERLIQUID_NETWORK=mainnet
225
248
  // ── Main ──
226
249
 
227
250
  async function main(): Promise<OnboardResult> {
251
+ if (cliArgs.includes('--help') || cliArgs.includes('-h')) {
252
+ console.log(`
253
+ OpenBroker Setup
254
+
255
+ Usage: openbroker setup [options]
256
+
257
+ Options:
258
+ -c, --config <path> Save config to a custom path (default: ~/.openbroker/.env)
259
+ --testnet Configure for testnet
260
+ --account-address <addr> Set HYPERLIQUID_ACCOUNT_ADDRESS (for API wallet / vault trading)
261
+ --help Show this help
262
+
263
+ Examples:
264
+ openbroker setup # Interactive → ~/.openbroker/.env
265
+ openbroker setup -c .env --testnet # Write to ./.env for testnet
266
+ openbroker setup -c ./testnet.env --testnet --account-address 0x... # API wallet config
267
+ `);
268
+ process.exit(0);
269
+ }
270
+
228
271
  console.log('OpenBroker - One-Command Setup');
229
272
  console.log('==============================\n');
273
+ if (cliConfigPath) console.log(`Config will be saved to: ${CONFIG_PATH}\n`);
274
+ if (useTestnet) console.log('Network: testnet\n');
230
275
  console.log('This will: 1) Create wallet 2) Save config 3) Approve builder fee\n');
231
276
 
232
277
  // Check if config already exists
@@ -253,7 +298,7 @@ async function main(): Promise<OnboardResult> {
253
298
  console.log(` API Wallet: ${account.address}`);
254
299
  console.log(` Master account address is missing — re-polling for approval...\n`);
255
300
 
256
- const approveUrl = `${OPENBROKER_URL}/approve?agent=${account.address}`;
301
+ const approveUrl = `${OPENBROKER_URL}/approve?agent=${account.address}${useTestnet ? '&network=testnet' : ''}`;
257
302
  console.log(` If not yet approved, visit: ${approveUrl}\n`);
258
303
 
259
304
  const masterAddress = await pollForApproval(account.address);
@@ -380,51 +425,57 @@ async function main(): Promise<OnboardResult> {
380
425
  console.log('Step 2/3: Creating config...');
381
426
  ensureConfigDir();
382
427
 
428
+ const network = useTestnet ? 'testnet' : 'mainnet';
429
+ const accountLine = cliAccountAddress ? `\n# Master/vault account address\nHYPERLIQUID_ACCOUNT_ADDRESS=${cliAccountAddress}\n` : '';
383
430
  const envContent = `# OpenBroker Configuration
384
- # Location: ~/.openbroker/.env
431
+ # Location: ${CONFIG_PATH}
385
432
  # WARNING: Keep this file secret! Never share it!
386
433
 
387
434
  # Your wallet private key
388
435
  HYPERLIQUID_PRIVATE_KEY=${privateKey}
389
-
436
+ ${accountLine}
390
437
  # Network: mainnet or testnet
391
- HYPERLIQUID_NETWORK=mainnet
438
+ HYPERLIQUID_NETWORK=${network}
392
439
  `;
393
440
 
394
441
  fs.writeFileSync(CONFIG_PATH, envContent, { mode: 0o600 });
395
442
  console.log(`✅ Config saved to: ${CONFIG_PATH}\n`);
396
443
 
397
- // Approve builder fee (automatic - no user action needed)
398
- console.log('Step 3/3: Approving builder fee...');
399
- console.log('(This is automatic, and required for trading)\n');
400
-
401
- try {
402
- // Import and run approve-builder inline
403
- const { getClient } = await import('../core/client.js');
404
- const client = getClient();
444
+ // Approve builder fee (automatic - no user action needed; skip on testnet)
445
+ if (useTestnet) {
446
+ console.log('Step 3/3: Skipping builder fee approval (testnet)\n');
447
+ } else {
448
+ console.log('Step 3/3: Approving builder fee...');
449
+ console.log('(This is automatic, and required for trading)\n');
405
450
 
406
- console.log(` Account: ${client.address}`);
407
- console.log(` Builder: ${OPEN_BROKER_BUILDER_ADDRESS}`);
451
+ try {
452
+ // Import and run approve-builder inline
453
+ const { getClient } = await import('../core/client.js');
454
+ const client = getClient();
408
455
 
409
- // Check if already approved
410
- const currentApproval = await client.getMaxBuilderFee(client.address, OPEN_BROKER_BUILDER_ADDRESS);
456
+ console.log(` Account: ${client.address}`);
457
+ console.log(` Builder: ${OPEN_BROKER_BUILDER_ADDRESS}`);
411
458
 
412
- if (currentApproval) {
413
- console.log(`\n✅ Builder fee already approved (${currentApproval})`);
414
- } else {
415
- console.log('\n Sending approval transaction...');
416
- const result = await client.approveBuilderFee('0.1%', OPEN_BROKER_BUILDER_ADDRESS);
459
+ // Check if already approved
460
+ const currentApproval = await client.getMaxBuilderFee(client.address, OPEN_BROKER_BUILDER_ADDRESS);
417
461
 
418
- if (result.status === 'ok') {
419
- console.log('✅ Builder fee approved successfully!');
462
+ if (currentApproval) {
463
+ console.log(`\n✅ Builder fee already approved (${currentApproval})`);
420
464
  } else {
421
- console.log(`⚠️ Approval may have failed: ${result.response}`);
422
- console.log(' You can retry later: openbroker approve-builder');
465
+ console.log('\n Sending approval transaction...');
466
+ const result = await client.approveBuilderFee('0.1%', OPEN_BROKER_BUILDER_ADDRESS);
467
+
468
+ if (result.status === 'ok') {
469
+ console.log('✅ Builder fee approved successfully!');
470
+ } else {
471
+ console.log(`⚠️ Approval may have failed: ${result.response}`);
472
+ console.log(' You can retry later: openbroker approve-builder');
473
+ }
423
474
  }
475
+ } catch (error) {
476
+ console.log(`⚠️ Could not approve builder fee: ${error}`);
477
+ console.log(' You can retry later: openbroker approve-builder');
424
478
  }
425
- } catch (error) {
426
- console.log(`⚠️ Could not approve builder fee: ${error}`);
427
- console.log(' You can retry later: openbroker approve-builder');
428
479
  }
429
480
 
430
481
  // Final summary