openbroker 1.0.55 → 1.0.56

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to Open Broker will be documented in this file.
4
4
 
5
+ ## [1.0.56] - 2026-03-10
6
+
7
+ ### Added
8
+ - **Unified Account Support**: Detects and handles Hyperliquid's account abstraction modes (standard, unified, portfolio margin, dex abstraction)
9
+ - `getAccountMode()` / `isUnifiedAccount()` — queries `userAbstraction` API to detect mode
10
+ - **Unified accounts**: equity comes from `spotClearinghouseState` (single USDC balance shared across all dexes); skips `sendAsset` transfers for HIP-3 trading
11
+ - **Standard accounts**: keeps existing behavior (per-dex margin aggregation + USDC transfers)
12
+ - `account` command now displays the account abstraction mode
13
+ - Prepares for deprecation of DEX abstraction mode per Hyperliquid docs
14
+
5
15
  ## [1.0.55] - 2026-03-09
6
16
 
7
17
  ### Added
package/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: Hyperliquid trading plugin with background position monitoring. Exe
4
4
  license: MIT
5
5
  compatibility: Requires Node.js 22+, network access to api.hyperliquid.xyz
6
6
  homepage: https://www.npmjs.com/package/openbroker
7
- metadata: {"author": "monemetrics", "version": "1.0.55", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
7
+ metadata: {"author": "monemetrics", "version": "1.0.56", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
8
8
  allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_twap ob_bracket ob_chase ob_watcher_status Bash(openbroker:*)
9
9
  ---
10
10
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.55",
4
+ "version": "1.0.56",
5
5
  "description": "Trade on Hyperliquid DEX with background position monitoring",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.0.55",
3
+ "version": "1.0.56",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,6 +35,8 @@ export class HyperliquidClient {
35
35
  private hip3IsolatedSet: Set<string> = new Set();
36
36
  /** Cached maxLeverage for HIP-3 assets */
37
37
  private hip3MaxLeverageMap: Map<string, number> = new Map();
38
+ /** Cached account abstraction mode: 'standard' | 'unified' | 'portfolio' | 'dexAbstraction' */
39
+ private accountMode: string | null = null;
38
40
  public verbose: boolean = false;
39
41
 
40
42
  constructor(config?: OpenBrokerConfig) {
@@ -631,6 +633,57 @@ export class HyperliquidClient {
631
633
 
632
634
  // ============ Account Info ============
633
635
 
636
+ /**
637
+ * Get the account's abstraction mode.
638
+ * Returns: 'standard' | 'unified' | 'portfolio' | 'dexAbstraction'
639
+ * Unified accounts have a single USDC balance shared across all dexes.
640
+ * Standard accounts have separate balances per dex (need sendAsset transfers).
641
+ */
642
+ async getAccountMode(user?: string): Promise<string> {
643
+ if (this.accountMode) return this.accountMode;
644
+
645
+ const baseUrl = isMainnet()
646
+ ? 'https://api.hyperliquid.xyz'
647
+ : 'https://api.hyperliquid-testnet.xyz';
648
+
649
+ try {
650
+ const response = await fetch(baseUrl + '/info', {
651
+ method: 'POST',
652
+ headers: { 'Content-Type': 'application/json' },
653
+ body: JSON.stringify({
654
+ type: 'userAbstraction',
655
+ user: user ?? this.address,
656
+ }),
657
+ });
658
+ const data = await response.json();
659
+ this.log('userAbstraction response:', data);
660
+
661
+ // API returns: "default" | "disabled" | "dexAbstraction" | "unifiedAccount" | "portfolioMargin"
662
+ if (data === 'unifiedAccount') {
663
+ this.accountMode = 'unified';
664
+ } else if (data === 'portfolioMargin') {
665
+ this.accountMode = 'portfolio';
666
+ } else if (data === 'dexAbstraction') {
667
+ this.accountMode = 'dexAbstraction';
668
+ } else {
669
+ // "default" or "disabled" both mean standard mode
670
+ this.accountMode = 'standard';
671
+ }
672
+ } catch (err) {
673
+ this.log('Failed to fetch account abstraction mode:', err instanceof Error ? err.message : String(err));
674
+ this.accountMode = 'standard'; // Safe fallback
675
+ }
676
+
677
+ this.log('Account mode:', this.accountMode);
678
+ return this.accountMode;
679
+ }
680
+
681
+ /** Whether the account uses unified balances (unified or portfolio margin) */
682
+ async isUnifiedAccount(user?: string): Promise<boolean> {
683
+ const mode = await this.getAccountMode(user);
684
+ return mode === 'unified' || mode === 'portfolio';
685
+ }
686
+
634
687
  /**
635
688
  * Check if an address has sub-accounts (is a master account)
636
689
  * Sub-accounts cannot approve builder fees - only master accounts can
@@ -1075,14 +1128,17 @@ export class HyperliquidClient {
1075
1128
 
1076
1129
  /**
1077
1130
  * Get user state across all dexes (main + HIP-3).
1078
- * Merges HIP-3 positions into assetPositions and aggregates margin summaries.
1131
+ * For unified accounts: equity comes from spotClearinghouseState (single USDC balance).
1132
+ * For standard accounts: aggregates margin summaries from each dex.
1079
1133
  */
1080
1134
  async getUserStateAll(user?: string): Promise<ClearinghouseState> {
1081
1135
  await this.getMetaAndAssetCtxs(); // Ensure HIP-3 dex list is loaded
1082
1136
 
1137
+ const unified = await this.isUnifiedAccount(user);
1083
1138
  const mainState = await this.getUserState(user);
1084
1139
  const dexs = await this.getPerpDexs();
1085
1140
 
1141
+ // Collect positions from all HIP-3 dexes
1086
1142
  for (let i = 1; i < dexs.length; i++) {
1087
1143
  const dex = dexs[i];
1088
1144
  if (!dex) continue;
@@ -1093,24 +1149,63 @@ export class HyperliquidClient {
1093
1149
  mainState.assetPositions.push(...dexState.assetPositions);
1094
1150
  }
1095
1151
 
1096
- // Aggregate margin summaries from HIP-3 dexes
1097
- const dexMargin = dexState.marginSummary;
1098
- if (dexMargin) {
1099
- const addToSummary = (summary: { accountValue: string; totalNtlPos: string; totalRawUsd: string; totalMarginUsed: string; withdrawable: string }) => {
1100
- summary.accountValue = String(parseFloat(summary.accountValue) + parseFloat(dexMargin.accountValue));
1101
- summary.totalNtlPos = String(parseFloat(summary.totalNtlPos) + parseFloat(dexMargin.totalNtlPos));
1102
- summary.totalRawUsd = String(parseFloat(summary.totalRawUsd) + parseFloat(dexMargin.totalRawUsd));
1103
- summary.totalMarginUsed = String(parseFloat(summary.totalMarginUsed) + parseFloat(dexMargin.totalMarginUsed));
1104
- summary.withdrawable = String(parseFloat(summary.withdrawable) + parseFloat(dexMargin.withdrawable));
1105
- };
1106
- addToSummary(mainState.marginSummary);
1107
- addToSummary(mainState.crossMarginSummary);
1152
+ // For standard accounts, aggregate margin from each dex
1153
+ if (!unified) {
1154
+ const dexMargin = dexState.marginSummary;
1155
+ if (dexMargin) {
1156
+ const addToSummary = (summary: { accountValue: string; totalNtlPos: string; totalRawUsd: string; totalMarginUsed: string; withdrawable: string }) => {
1157
+ summary.accountValue = String(parseFloat(summary.accountValue) + parseFloat(dexMargin.accountValue));
1158
+ summary.totalNtlPos = String(parseFloat(summary.totalNtlPos) + parseFloat(dexMargin.totalNtlPos));
1159
+ summary.totalRawUsd = String(parseFloat(summary.totalRawUsd) + parseFloat(dexMargin.totalRawUsd));
1160
+ summary.totalMarginUsed = String(parseFloat(summary.totalMarginUsed) + parseFloat(dexMargin.totalMarginUsed));
1161
+ summary.withdrawable = String(parseFloat(summary.withdrawable) + parseFloat(dexMargin.withdrawable));
1162
+ };
1163
+ addToSummary(mainState.marginSummary);
1164
+ addToSummary(mainState.crossMarginSummary);
1165
+ }
1108
1166
  }
1109
1167
  } catch (err) {
1110
1168
  this.log(`Failed to fetch state for dex ${dex.name}:`, err instanceof Error ? err.message : String(err));
1111
1169
  }
1112
1170
  }
1113
1171
 
1172
+ // For unified accounts: equity is the USDC balance from spot clearinghouse
1173
+ if (unified) {
1174
+ try {
1175
+ const spotState = await this.getSpotBalances(user);
1176
+ const usdcBalance = spotState.balances.find(b => b.coin === 'USDC');
1177
+ if (usdcBalance) {
1178
+ const totalUsdc = usdcBalance.total;
1179
+ const holdUsdc = usdcBalance.hold;
1180
+ const withdrawable = String(parseFloat(totalUsdc) - parseFloat(holdUsdc));
1181
+
1182
+ // Compute total margin used and notional from all positions
1183
+ let totalMarginUsed = 0;
1184
+ let totalNtlPos = 0;
1185
+ for (const ap of mainState.assetPositions) {
1186
+ const pos = ap.position;
1187
+ if (parseFloat(pos.szi) === 0) continue;
1188
+ totalMarginUsed += parseFloat(pos.marginUsed);
1189
+ totalNtlPos += Math.abs(parseFloat(pos.positionValue));
1190
+ }
1191
+
1192
+ const summary = {
1193
+ accountValue: totalUsdc,
1194
+ totalNtlPos: String(totalNtlPos),
1195
+ totalRawUsd: totalUsdc,
1196
+ totalMarginUsed: String(totalMarginUsed),
1197
+ withdrawable,
1198
+ };
1199
+ mainState.marginSummary = summary;
1200
+ mainState.crossMarginSummary = { ...summary };
1201
+
1202
+ this.log(`Unified account: USDC balance $${parseFloat(totalUsdc).toFixed(2)}, margin used $${totalMarginUsed.toFixed(2)}`);
1203
+ }
1204
+ } catch (err) {
1205
+ this.log('Failed to fetch spot balances for unified account:', err instanceof Error ? err.message : String(err));
1206
+ }
1207
+ }
1208
+
1114
1209
  return mainState;
1115
1210
  }
1116
1211
 
@@ -1123,9 +1218,10 @@ export class HyperliquidClient {
1123
1218
  // ============ Trading ============
1124
1219
 
1125
1220
  /**
1126
- * HIP-3 perps have independent margin per dex. Before ordering:
1221
+ * HIP-3 perps: prepare for trading.
1127
1222
  * 1. Set isolated margin mode (required for HIP-3)
1128
- * 2. Transfer USDC from main perp to the HIP-3 dex (each dex has its own balance)
1223
+ * 2. For standard accounts only: transfer USDC from main perp to HIP-3 dex
1224
+ * (unified accounts share USDC across all dexes automatically)
1129
1225
  */
1130
1226
  private async ensureHip3Ready(coin: string, notional: number, leverage?: number): Promise<void> {
1131
1227
  if (!this.isHip3(coin)) return;
@@ -1148,12 +1244,19 @@ export class HyperliquidClient {
1148
1244
  }
1149
1245
  }
1150
1246
 
1151
- // Transfer USDC to the HIP-3 dex to cover margin
1247
+ // Unified accounts share USDC across all dexes no transfer needed
1248
+ const unified = await this.isUnifiedAccount();
1249
+ if (unified) {
1250
+ this.log(`Unified account — skipping USDC transfer for ${coin} (shared balance)`);
1251
+ return;
1252
+ }
1253
+
1254
+ // Standard accounts: transfer USDC to the HIP-3 dex to cover margin
1152
1255
  const requiredMargin = notional / effectiveLev;
1153
1256
  // Add 20% buffer for fees and slippage
1154
1257
  const transferAmount = Math.ceil(requiredMargin * 1.2 * 100) / 100;
1155
1258
 
1156
- this.log(`HIP-3 margin transfer: ${transferAmount} USDC from main → ${dexInfo.dexName} (notional: ${notional}, leverage: ${maxLev}x)`);
1259
+ this.log(`HIP-3 margin transfer: ${transferAmount} USDC from main → ${dexInfo.dexName} (notional: ${notional}, leverage: ${effectiveLev}x)`);
1157
1260
  try {
1158
1261
  await this.exchange.sendAsset({
1159
1262
  destination: this.address as `0x${string}`,
@@ -17,6 +17,15 @@ async function main() {
17
17
  console.log(`Signing Wallet: ${client.walletAddress}`);
18
18
  console.log(`Wallet Type: ${client.isApiWallet ? 'API Wallet' : 'Main Wallet'}`);
19
19
 
20
+ const accountMode = await client.getAccountMode();
21
+ const modeLabel: Record<string, string> = {
22
+ standard: 'Standard (separate balances per dex)',
23
+ unified: 'Unified Account (shared USDC across all dexes)',
24
+ portfolio: 'Portfolio Margin',
25
+ dexAbstraction: 'DEX Abstraction (deprecated)',
26
+ };
27
+ console.log(`Account Mode: ${modeLabel[accountMode] ?? accountMode}`);
28
+
20
29
  // Check builder fee approval
21
30
  const builderApproval = await client.getMaxBuilderFee();
22
31
  console.log(`Builder Address: ${client.builderAddress}`);