openbroker 1.0.33

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,92 @@
1
+ // Configuration loader for Open Broker
2
+
3
+ import { config as loadDotenv } from 'dotenv';
4
+ import { resolve } from 'path';
5
+ import { existsSync } from 'fs';
6
+ import { privateKeyToAccount } from 'viem/accounts';
7
+ import type { OpenBrokerConfig } from './types.js';
8
+
9
+ // Get project root relative to this file (scripts/core/config.ts -> project root)
10
+ export const PROJECT_ROOT = resolve(import.meta.dirname, '../..');
11
+ export const ENV_PATH = resolve(PROJECT_ROOT, '.env');
12
+
13
+ // Load .env from project root (silently skip if doesn't exist)
14
+ if (existsSync(ENV_PATH)) {
15
+ // Set DOTENV_CONFIG_QUIET to suppress dotenv's default logging
16
+ process.env.DOTENV_CONFIG_QUIET = 'true';
17
+ const result = loadDotenv({ path: ENV_PATH });
18
+
19
+ if (process.env.VERBOSE === '1' || process.env.VERBOSE === 'true') {
20
+ console.log(`[DEBUG] Loading .env from: ${ENV_PATH}`);
21
+ console.log(`[DEBUG] .env loaded: ${result.parsed ? 'yes' : 'no'}`);
22
+ }
23
+ } else if (process.env.VERBOSE === '1' || process.env.VERBOSE === 'true') {
24
+ console.log(`[DEBUG] No .env file found at: ${ENV_PATH}`);
25
+ console.log(`[DEBUG] Run 'npx tsx scripts/setup/onboard.ts' to create one`);
26
+ }
27
+
28
+ const MAINNET_URL = 'https://api.hyperliquid.xyz';
29
+ const TESTNET_URL = 'https://api.hyperliquid-testnet.xyz';
30
+
31
+ // Open Broker builder address - receives builder fees on all trades
32
+ // This funds continued development of the open-broker project
33
+ export const OPEN_BROKER_BUILDER_ADDRESS = '0xbb67021fA3e62ab4DA985bb5a55c5c1884381068';
34
+
35
+ export function loadConfig(): OpenBrokerConfig {
36
+ const privateKey = process.env.HYPERLIQUID_PRIVATE_KEY;
37
+ if (!privateKey) {
38
+ if (!existsSync(ENV_PATH)) {
39
+ throw new Error(
40
+ 'No .env file found. Run onboarding first:\n\n' +
41
+ ' npx tsx scripts/setup/onboard.ts\n'
42
+ );
43
+ }
44
+ throw new Error(
45
+ 'HYPERLIQUID_PRIVATE_KEY not found in .env file.\n' +
46
+ 'Add it to your .env or run: npx tsx scripts/setup/onboard.ts'
47
+ );
48
+ }
49
+
50
+ if (!privateKey.startsWith('0x') || privateKey.length !== 66) {
51
+ throw new Error('HYPERLIQUID_PRIVATE_KEY must be a 64-character hex string with 0x prefix');
52
+ }
53
+
54
+ const network = process.env.HYPERLIQUID_NETWORK || 'mainnet';
55
+ const baseUrl = network === 'testnet' ? TESTNET_URL : MAINNET_URL;
56
+
57
+ // Use open-broker address by default, but allow override for custom builders
58
+ const builderAddress = (process.env.BUILDER_ADDRESS || OPEN_BROKER_BUILDER_ADDRESS).toLowerCase();
59
+ const builderFee = parseInt(process.env.BUILDER_FEE || '10', 10); // default 1 bps
60
+ const slippageBps = parseInt(process.env.SLIPPAGE_BPS || '50', 10); // default 0.5%
61
+
62
+ // Derive the wallet address from private key
63
+ const wallet = privateKeyToAccount(privateKey as `0x${string}`);
64
+ const walletAddress = wallet.address.toLowerCase();
65
+
66
+ // Account address can be different if using an API wallet
67
+ // If not specified, use the wallet address itself
68
+ const accountAddress = process.env.HYPERLIQUID_ACCOUNT_ADDRESS?.toLowerCase();
69
+
70
+ // Determine if this is an API wallet setup
71
+ const isApiWallet = accountAddress !== undefined && accountAddress !== walletAddress;
72
+
73
+ return {
74
+ baseUrl,
75
+ privateKey: privateKey as `0x${string}`,
76
+ walletAddress,
77
+ accountAddress: accountAddress || walletAddress,
78
+ isApiWallet,
79
+ builderAddress,
80
+ builderFee,
81
+ slippageBps,
82
+ };
83
+ }
84
+
85
+ export function getNetwork(): 'mainnet' | 'testnet' {
86
+ const network = process.env.HYPERLIQUID_NETWORK || 'mainnet';
87
+ return network === 'testnet' ? 'testnet' : 'mainnet';
88
+ }
89
+
90
+ export function isMainnet(): boolean {
91
+ return getNetwork() === 'mainnet';
92
+ }
@@ -0,0 +1,192 @@
1
+ // Hyperliquid API Types for Open Broker
2
+
3
+ // ============ Configuration ============
4
+
5
+ export interface OpenBrokerConfig {
6
+ baseUrl: string;
7
+ privateKey: `0x${string}`;
8
+ walletAddress: string; // Address derived from private key (the signer)
9
+ accountAddress: string; // Address to trade on behalf of (may differ if using API wallet)
10
+ isApiWallet: boolean; // True if walletAddress != accountAddress
11
+ builderAddress: string;
12
+ builderFee: number; // tenths of bps (10 = 1 bps)
13
+ slippageBps: number;
14
+ }
15
+
16
+ // ============ Builder ============
17
+
18
+ export interface BuilderInfo {
19
+ b: string; // builder address (lowercase)
20
+ f: number; // fee in tenths of bps
21
+ }
22
+
23
+ // ============ Order Types ============
24
+
25
+ export type OrderType =
26
+ | { limit: { tif: 'Gtc' | 'Ioc' | 'Alo' } }
27
+ | { trigger: { triggerPx: string; isMarket: boolean; tpsl: 'tp' | 'sl' } };
28
+
29
+ export interface OrderRequest {
30
+ coin: string;
31
+ is_buy: boolean;
32
+ sz: number;
33
+ limit_px: number;
34
+ order_type: OrderType;
35
+ reduce_only?: boolean;
36
+ cloid?: string;
37
+ }
38
+
39
+ export interface OrderWire {
40
+ a: number; // asset index
41
+ b: boolean; // is_buy
42
+ p: string; // price
43
+ s: string; // size
44
+ r: boolean; // reduce_only
45
+ t: OrderType;
46
+ c?: string; // cloid
47
+ }
48
+
49
+ export interface OrderResponse {
50
+ status: 'ok' | 'err';
51
+ response?: {
52
+ type: 'order';
53
+ data: {
54
+ statuses: Array<{
55
+ resting?: { oid: number };
56
+ filled?: { totalSz: string; avgPx: string; oid: number };
57
+ error?: string;
58
+ [key: string]: unknown; // Catch any other fields
59
+ }>;
60
+ };
61
+ } | string; // API can return string error message
62
+ error?: string;
63
+ }
64
+
65
+ // ============ Cancel Types ============
66
+
67
+ export interface CancelRequest {
68
+ coin: string;
69
+ oid: number;
70
+ }
71
+
72
+ export interface CancelResponse {
73
+ status: 'ok' | 'err';
74
+ response?: {
75
+ type: 'cancel';
76
+ data: { statuses: string[] };
77
+ };
78
+ }
79
+
80
+ // ============ Market Data Types ============
81
+
82
+ export interface AssetMeta {
83
+ name: string;
84
+ szDecimals: number;
85
+ maxLeverage: number;
86
+ onlyIsolated: boolean;
87
+ }
88
+
89
+ export interface AssetCtx {
90
+ funding: string;
91
+ openInterest: string;
92
+ prevDayPx: string;
93
+ dayNtlVlm: string;
94
+ premium: string;
95
+ oraclePx: string;
96
+ markPx: string;
97
+ midPx?: string;
98
+ impactPxs?: [string, string];
99
+ }
100
+
101
+ export interface Meta {
102
+ universe: AssetMeta[];
103
+ }
104
+
105
+ export interface MetaAndAssetCtxs {
106
+ meta: Meta;
107
+ assetCtxs: AssetCtx[];
108
+ }
109
+
110
+ // ============ Account Types ============
111
+
112
+ export interface Position {
113
+ coin: string;
114
+ szi: string; // signed size (negative = short)
115
+ entryPx: string;
116
+ positionValue: string;
117
+ unrealizedPnl: string;
118
+ returnOnEquity: string;
119
+ liquidationPx: string | null;
120
+ leverage: {
121
+ type: 'cross' | 'isolated';
122
+ value: number;
123
+ rawUsd?: string;
124
+ };
125
+ marginUsed: string;
126
+ maxLeverage: number;
127
+ }
128
+
129
+ export interface AssetPosition {
130
+ position: Position;
131
+ type: 'oneWay';
132
+ }
133
+
134
+ export interface MarginSummary {
135
+ accountValue: string;
136
+ totalNtlPos: string;
137
+ totalRawUsd: string;
138
+ totalMarginUsed: string;
139
+ withdrawable: string;
140
+ }
141
+
142
+ export interface ClearinghouseState {
143
+ assetPositions: AssetPosition[];
144
+ crossMarginSummary: MarginSummary;
145
+ marginSummary: MarginSummary;
146
+ crossMaintenanceMarginUsed: string;
147
+ }
148
+
149
+ // ============ Funding Types ============
150
+
151
+ export interface FundingInfo {
152
+ coin: string;
153
+ fundingRate: string; // hourly rate
154
+ annualizedRate: number; // calculated
155
+ premium: string;
156
+ openInterest: string;
157
+ markPx: string;
158
+ oraclePx: string;
159
+ }
160
+
161
+ // ============ Open Orders ============
162
+
163
+ export interface OpenOrder {
164
+ coin: string;
165
+ oid: number;
166
+ cloid?: string;
167
+ side: 'B' | 'A'; // Bid or Ask
168
+ sz: string;
169
+ limitPx: string;
170
+ orderType: string;
171
+ origSz: string;
172
+ timestamp: number;
173
+ }
174
+
175
+ // ============ API Request/Response ============
176
+
177
+ export interface InfoRequest {
178
+ type: string;
179
+ user?: string;
180
+ [key: string]: unknown;
181
+ }
182
+
183
+ export interface ExchangeRequest {
184
+ action: Record<string, unknown>;
185
+ nonce: number;
186
+ signature: {
187
+ r: string;
188
+ s: string;
189
+ v: number;
190
+ };
191
+ vaultAddress?: string | null;
192
+ }
@@ -0,0 +1,156 @@
1
+ // Utility functions for Open Broker
2
+
3
+ import type { OrderType, OrderWire, OrderRequest } from './types.js';
4
+
5
+ /**
6
+ * Round price to 5 significant figures and appropriate decimals
7
+ * Perps: max 6 decimals, Spot: max 8 decimals
8
+ */
9
+ export function roundPrice(price: number, szDecimals: number, isSpot: boolean = false): string {
10
+ // Round to 5 significant figures first
11
+ const sigFigs = parseFloat(price.toPrecision(5));
12
+ // Then round to max decimals (6 for perps - szDecimals adjustment, 8 for spot)
13
+ const maxDecimals = isSpot ? 8 : Math.max(0, 6 - szDecimals);
14
+ return sigFigs.toFixed(maxDecimals);
15
+ }
16
+
17
+ /**
18
+ * Round size to asset's szDecimals
19
+ */
20
+ export function roundSize(size: number, szDecimals: number): string {
21
+ return size.toFixed(szDecimals);
22
+ }
23
+
24
+ /**
25
+ * Calculate slippage-adjusted price for market orders
26
+ */
27
+ export function getSlippagePrice(
28
+ midPrice: number,
29
+ isBuy: boolean,
30
+ slippageBps: number
31
+ ): number {
32
+ const slippageMultiplier = slippageBps / 10000;
33
+ return isBuy
34
+ ? midPrice * (1 + slippageMultiplier)
35
+ : midPrice * (1 - slippageMultiplier);
36
+ }
37
+
38
+ /**
39
+ * Convert order request to wire format
40
+ */
41
+ export function orderToWire(
42
+ order: OrderRequest,
43
+ assetIndex: number,
44
+ szDecimals: number
45
+ ): OrderWire {
46
+ const wire: OrderWire = {
47
+ a: assetIndex,
48
+ b: order.is_buy,
49
+ p: roundPrice(order.limit_px, szDecimals),
50
+ s: roundSize(order.sz, szDecimals),
51
+ r: order.reduce_only ?? false,
52
+ t: order.order_type,
53
+ };
54
+
55
+ if (order.cloid) {
56
+ wire.c = order.cloid;
57
+ }
58
+
59
+ return wire;
60
+ }
61
+
62
+ /**
63
+ * Get current timestamp in milliseconds
64
+ */
65
+ export function getTimestampMs(): number {
66
+ return Date.now();
67
+ }
68
+
69
+ /**
70
+ * Format USD amount for display
71
+ */
72
+ export function formatUsd(amount: number | string): string {
73
+ const num = typeof amount === 'string' ? parseFloat(amount) : amount;
74
+ return new Intl.NumberFormat('en-US', {
75
+ style: 'currency',
76
+ currency: 'USD',
77
+ minimumFractionDigits: 2,
78
+ maximumFractionDigits: 2,
79
+ }).format(num);
80
+ }
81
+
82
+ /**
83
+ * Format percentage for display
84
+ */
85
+ export function formatPercent(value: number | string, decimals: number = 2): string {
86
+ const num = typeof value === 'string' ? parseFloat(value) : value;
87
+ return `${(num * 100).toFixed(decimals)}%`;
88
+ }
89
+
90
+ /**
91
+ * Format funding rate (hourly to annualized)
92
+ */
93
+ export function annualizeFundingRate(hourlyRate: number | string): number {
94
+ const rate = typeof hourlyRate === 'string' ? parseFloat(hourlyRate) : hourlyRate;
95
+ return rate * 8760; // 24 * 365 hours
96
+ }
97
+
98
+ /**
99
+ * Parse CLI arguments into key-value pairs
100
+ */
101
+ export function parseArgs(args: string[]): Record<string, string | boolean> {
102
+ const result: Record<string, string | boolean> = {};
103
+
104
+ for (let i = 0; i < args.length; i++) {
105
+ const arg = args[i];
106
+ if (arg.startsWith('--')) {
107
+ const key = arg.slice(2);
108
+ const nextArg = args[i + 1];
109
+
110
+ if (nextArg && !nextArg.startsWith('--')) {
111
+ result[key] = nextArg;
112
+ i++;
113
+ } else {
114
+ result[key] = true;
115
+ }
116
+ }
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ /**
123
+ * Sleep for specified milliseconds
124
+ */
125
+ export function sleep(ms: number): Promise<void> {
126
+ return new Promise(resolve => setTimeout(resolve, ms));
127
+ }
128
+
129
+ /**
130
+ * Generate a random client order ID
131
+ */
132
+ export function generateCloid(): string {
133
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
134
+ let result = '';
135
+ for (let i = 0; i < 16; i++) {
136
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
137
+ }
138
+ return result;
139
+ }
140
+
141
+ /**
142
+ * Check if builder fee is approved and print warning if not
143
+ * Returns true if approved, false if not
144
+ */
145
+ export async function checkBuilderFeeApproval(
146
+ client: { getMaxBuilderFee: () => Promise<string | null>; builderAddress: string }
147
+ ): Promise<boolean> {
148
+ const approval = await client.getMaxBuilderFee();
149
+ if (!approval) {
150
+ console.log('⚠️ Builder fee not approved!');
151
+ console.log(` Run: npx tsx scripts/setup/approve-builder.ts`);
152
+ console.log(` Builder: ${client.builderAddress}\n`);
153
+ return false;
154
+ }
155
+ return true;
156
+ }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env npx tsx
2
+ // Get account info from Hyperliquid
3
+
4
+ import { getClient } from '../core/client.js';
5
+ import { formatUsd, formatPercent, parseArgs } from '../core/utils.js';
6
+
7
+ async function main() {
8
+ const args = parseArgs(process.argv.slice(2));
9
+ const client = getClient();
10
+
11
+ console.log('Open Broker - Account Info');
12
+ console.log('==========================\n');
13
+
14
+ console.log('Wallet Configuration');
15
+ console.log('--------------------');
16
+ console.log(`Trading Account: ${client.address}`);
17
+ console.log(`Signing Wallet: ${client.walletAddress}`);
18
+ console.log(`Wallet Type: ${client.isApiWallet ? 'API Wallet' : 'Main Wallet'}`);
19
+
20
+ // Check builder fee approval
21
+ const builderApproval = await client.getMaxBuilderFee();
22
+ console.log(`Builder Address: ${client.builderAddress}`);
23
+ console.log(`Builder Fee: ${client.builderFeeBps} bps`);
24
+ if (builderApproval) {
25
+ console.log(`Builder Approved: ✅ Yes (max: ${builderApproval})`);
26
+ } else {
27
+ console.log(`Builder Approved: ❌ No`);
28
+ console.log(`\n⚠️ Run: npx tsx scripts/setup/approve-builder.ts`);
29
+ }
30
+ console.log('');
31
+
32
+ try {
33
+ const state = await client.getUserState();
34
+
35
+ const margin = state.crossMarginSummary;
36
+ const accountValue = parseFloat(margin.accountValue);
37
+ const totalMarginUsed = parseFloat(margin.totalMarginUsed);
38
+ const withdrawable = parseFloat(margin.withdrawable);
39
+ const totalNotional = parseFloat(margin.totalNtlPos);
40
+
41
+ console.log('Margin Summary');
42
+ console.log('--------------');
43
+ console.log(`Account Value: ${formatUsd(accountValue)}`);
44
+ console.log(`Total Notional: ${formatUsd(totalNotional)}`);
45
+ console.log(`Margin Used: ${formatUsd(totalMarginUsed)}`);
46
+ console.log(`Withdrawable: ${formatUsd(withdrawable)}`);
47
+
48
+ if (totalMarginUsed > 0) {
49
+ const marginRatio = totalMarginUsed / accountValue;
50
+ console.log(`Margin Ratio: ${formatPercent(marginRatio)}`);
51
+ }
52
+
53
+ console.log('\nPositions Summary');
54
+ console.log('-----------------');
55
+
56
+ if (state.assetPositions.length === 0) {
57
+ console.log('No open positions');
58
+ } else {
59
+ let totalPnl = 0;
60
+ console.log('Coin | Size | Entry | Mark | PnL | Leverage');
61
+ console.log('---------|------------|------------|------------|------------|----------');
62
+
63
+ for (const ap of state.assetPositions) {
64
+ const pos = ap.position;
65
+ const size = parseFloat(pos.szi);
66
+ if (Math.abs(size) < 0.0001) continue;
67
+
68
+ const entryPx = parseFloat(pos.entryPx);
69
+ const pnl = parseFloat(pos.unrealizedPnl);
70
+ totalPnl += pnl;
71
+
72
+ // Get mark price from leverage calculation
73
+ const notional = parseFloat(pos.positionValue);
74
+ const markPx = Math.abs(notional / size);
75
+
76
+ const side = size > 0 ? 'L' : 'S';
77
+ const leverageStr = `${pos.leverage.value}x ${pos.leverage.type}`;
78
+
79
+ console.log(
80
+ `${pos.coin.padEnd(8)} | ${side} ${Math.abs(size).toFixed(4).padStart(8)} | ` +
81
+ `${formatUsd(entryPx).padStart(10)} | ${formatUsd(markPx).padStart(10)} | ` +
82
+ `${formatUsd(pnl).padStart(10)} | ${leverageStr}`
83
+ );
84
+ }
85
+
86
+ console.log('---------|------------|------------|------------|------------|----------');
87
+ console.log(`Total Unrealized PnL: ${formatUsd(totalPnl)}`);
88
+ }
89
+
90
+ // Show open orders if requested
91
+ if (args.orders) {
92
+ console.log('\nOpen Orders');
93
+ console.log('-----------');
94
+
95
+ const orders = await client.getOpenOrders();
96
+ if (orders.length === 0) {
97
+ console.log('No open orders');
98
+ } else {
99
+ console.log('Coin | Side | Size | Price | Type');
100
+ console.log('---------|------|------------|------------|------');
101
+ for (const order of orders) {
102
+ const side = order.side === 'B' ? 'BUY ' : 'SELL';
103
+ console.log(
104
+ `${order.coin.padEnd(8)} | ${side} | ${parseFloat(order.sz).toFixed(4).padStart(10)} | ` +
105
+ `${formatUsd(parseFloat(order.limitPx)).padStart(10)} | ${order.orderType}`
106
+ );
107
+ }
108
+ }
109
+ }
110
+
111
+ } catch (error) {
112
+ console.error('Error fetching account info:', error);
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ main();