@reefclaw/openclaw-plugin 0.1.7 → 0.1.8

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/index.js CHANGED
@@ -1181,9 +1181,16 @@ const paperTradingPlugin = {
1181
1181
  // ingest credentials. See docs/APPROVAL_MODE_DESIGN.md §2 + §7.3.
1182
1182
  let proposalDecisionListener;
1183
1183
  if (tradingMode === 'MICRO_LIVE' || tradingMode === 'LIVE') {
1184
- if (!exchangeConfig) {
1184
+ // Per-VENUE credential presence (rehearsal find #4): this safety check
1185
+ // predated the venue seam and tested only the Binance shape, so a
1186
+ // PERSISTED HL MICRO_LIVE (tradingMode written by the runtime flip)
1187
+ // silently fell back to PAPER on every reboot — while the same config
1188
+ // flipped live fine at runtime. The per-venue gate above is the real
1189
+ // validation; this remains the defense-in-depth backstop.
1190
+ const liveCredsPresent = venue === 'hyperliquid' ? hlCredentials !== null : exchangeConfig !== null;
1191
+ if (!liveCredsPresent) {
1185
1192
  // Already handled above (falls back to PAPER), but safety check
1186
- logger.error(TAG, `${tradingMode} mode requires exchange config — this should not happen`);
1193
+ logger.error(TAG, `${tradingMode} mode requires ${venue} exchange credentials — this should not happen`);
1187
1194
  adapter = new PaperAdapter(simulator);
1188
1195
  tradingMode = 'PAPER';
1189
1196
  }
@@ -3,6 +3,8 @@ import type { ExchangeConfig, TradingMode } from '../types.js';
3
3
  import type { ExchangeSimulator } from '../simulator/exchange-simulator.js';
4
4
  import type { PaperMarketFeed } from '../simulator/paper-market-feed.js';
5
5
  import { LiveAdapter } from '../live/live-adapter.js';
6
+ import type { HlCredentials } from '../venues/hyperliquid/hl-private.js';
7
+ import { type VenueId } from '../venues/registry.js';
6
8
  import { PositionWatcher } from '../live/stop-watcher.js';
7
9
  import type { TradingOperationLock } from '../lifecycle/trading-operation-lock.js';
8
10
  export interface MicroLiveConfig {
@@ -14,6 +16,14 @@ export interface BuildAdapterInput {
14
16
  exchange: ExchangeConfig | null;
15
17
  microLive?: MicroLiveConfig;
16
18
  simulator: ExchangeSimulator;
19
+ /** Venue seam for the RUNTIME transition path (issue #217). Absent →
20
+ * 'binance' (every pre-venue caller byte-identical). The BOOT path got
21
+ * this seam in Phase 3 (createLiveAdapter in index.ts); the runtime flip
22
+ * — set_trading_mode, the product's go-live moment — missed it and
23
+ * constructed a Binance LiveAdapter unconditionally. */
24
+ venue?: VenueId;
25
+ /** Required for a live-mode build on the hyperliquid venue. */
26
+ hlCredentials?: HlCredentials | null;
17
27
  }
18
28
  /** Wave 9 safety wiring is created only after its durable ledger is loaded.
19
29
  * Runtime reapplies these hooks to both the bootstrap objects and every
@@ -81,6 +91,8 @@ export declare class PluginRuntime {
81
91
  mode: TradingMode;
82
92
  exchange: ExchangeConfig | null;
83
93
  microLive?: MicroLiveConfig;
94
+ venue?: VenueId;
95
+ hlCredentials?: HlCredentials | null;
84
96
  }, deps: {
85
97
  adapterDeps: {
86
98
  adapter: IExchangeAdapter;
@@ -13,9 +13,11 @@
13
13
  // instant — no gateway restart required.
14
14
  import { PaperAdapter } from '../paper-adapter.js';
15
15
  import { LiveAdapter } from '../live/live-adapter.js';
16
+ import { HyperliquidLiveAdapter } from '../venues/hyperliquid/hl-live-adapter.js';
17
+ import { createLiveAdapter } from '../venues/registry.js';
16
18
  import { PositionWatcher } from '../live/stop-watcher.js';
17
19
  import { loadBracketMode } from '../config/brackets-config.js';
18
- import { loadStopWatcherIntervalMs } from '../config/plugin-config-io.js';
20
+ import { loadStopWatcherIntervalMs, readPluginConfig } from '../config/plugin-config-io.js';
19
21
  import { logger, formatError } from '../logger.js';
20
22
  const TAG = 'plugin-runtime';
21
23
  /** Pure-ish factory: builds an adapter for the requested mode.
@@ -29,6 +31,23 @@ export function buildAdapter(input) {
29
31
  // ShadowTracker wraps a BinancePrivateApi for real-balance comparison.
30
32
  return new PaperAdapter(simulator);
31
33
  }
34
+ // Hyperliquid live (issue #217): mirror the boot path's construction —
35
+ // per-venue credential shape, the SAME factory (createLiveAdapter), and
36
+ // the same fall-back-to-paper defense when credentials are absent.
37
+ if (input.venue === 'hyperliquid') {
38
+ if (!input.hlCredentials) {
39
+ logger.warn(TAG, `${mode} on hyperliquid requested without walletAddress+agentPrivateKey — falling back to PAPER`);
40
+ return new PaperAdapter(simulator);
41
+ }
42
+ return createLiveAdapter({
43
+ venue: 'hyperliquid',
44
+ args: {
45
+ credentials: input.hlCredentials,
46
+ mode: mode,
47
+ marketSlippagePct: readPluginConfig().hl?.marketSlippagePct,
48
+ },
49
+ });
50
+ }
32
51
  if (!exchange) {
33
52
  logger.warn(TAG, `${mode} requested without credentials — falling back to PAPER`);
34
53
  return new PaperAdapter(simulator);
@@ -131,12 +150,24 @@ export class PluginRuntime {
131
150
  logger.warn(TAG, `old adapter shutdown failed: ${formatError(err)}`);
132
151
  }
133
152
  }
153
+ else if (old instanceof HyperliquidLiveAdapter) {
154
+ // Stops the HL user stream + the 60s REST truth-check timer — without
155
+ // this, a flip AWAY from HL live leaks both (issue #217).
156
+ try {
157
+ old.stop();
158
+ }
159
+ catch (err) {
160
+ logger.warn(TAG, `old HL adapter stop failed: ${formatError(err)}`);
161
+ }
162
+ }
134
163
  // 3. Build the new adapter.
135
164
  const fresh = buildAdapter({
136
165
  mode: next.mode,
137
166
  exchange: next.exchange,
138
167
  microLive: next.microLive,
139
168
  simulator: this.simulator,
169
+ venue: next.venue,
170
+ hlCredentials: next.hlCredentials,
140
171
  });
141
172
  // Install autonomous protection callbacks before initialization can emit
142
173
  // user-data or bracket-reconciler events.
@@ -145,7 +176,7 @@ export class PluginRuntime {
145
176
  }
146
177
  // 4. Fire async init for live adapters (non-blocking — readiness flips
147
178
  // INIT_PENDING → READY/DEGRADED/BLOCKED on its own).
148
- if (fresh instanceof LiveAdapter) {
179
+ if (fresh instanceof LiveAdapter || fresh instanceof HyperliquidLiveAdapter) {
149
180
  fresh.initialize().catch(err => {
150
181
  logger.error(TAG, `New adapter init failed: ${formatError(err)}`);
151
182
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "reefclaw-paper-trading",
3
3
  "name": "ReefClaw Paper Trading",
4
- "version": "0.1.0",
4
+ "version": "0.1.8",
5
5
  "description": "Paper trading plugin with real Binance market data and simulated execution. No API keys required.",
6
6
  "author": "ReefClaw",
7
7
  "activation": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/openclaw-plugin",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "ReefClaw trading plugin for OpenClaw \u2014 paper trading with real Binance market data, plus the ReefClaw dashboard connector (supervised by OpenClaw, no service manager needed). Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -66,16 +66,32 @@ if (!existsSync(pluginDist)) {
66
66
  console.log('[assemble] plugin/dist -> package root');
67
67
  }
68
68
 
69
- // 2. Manifest, with the `skills` declaration injected (npm package only).
69
+ // 2. Manifest, with the `skills` declaration injected (npm package only) and the
70
+ // release version stamped from THIS package's package.json.
71
+ //
72
+ // package.json#version is the authoritative release version — that is what
73
+ // ClawHub's `package-manifest-version-drift` rule compares against, and what
74
+ // OpenClaw's own `openclaw plugins build` writes into the manifest. The repo
75
+ // manifest (plugin/openclaw.plugin.json) carries a workspace-local version
76
+ // nobody bumps at release time, so copying it verbatim published
77
+ // manifest:0.1.0 against package:0.1.7 and tripped the validator. Deriving it
78
+ // here makes that drift structurally impossible instead of a bump to remember.
70
79
  const manifestSrc = join(repoRoot, 'plugin', 'openclaw.plugin.json');
80
+ const pkgVersion = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf-8')).version;
71
81
  if (!existsSync(manifestSrc)) {
72
82
  console.error('[assemble] missing plugin/openclaw.plugin.json.');
73
83
  missing += 1;
84
+ } else if (typeof pkgVersion !== 'string' || pkgVersion.length === 0) {
85
+ console.error('[assemble] package.json has no version — cannot stamp the manifest.');
86
+ missing += 1;
74
87
  } else {
75
88
  const manifest = JSON.parse(readFileSync(manifestSrc, 'utf-8'));
89
+ manifest.version = pkgVersion;
76
90
  manifest.skills = ['./skills'];
77
91
  writeFileSync(join(pkgRoot, 'openclaw.plugin.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
78
- console.log(`[assemble] manifest (+skills decl), ${manifest.contracts?.tools?.length ?? 0} contract tools`);
92
+ console.log(
93
+ `[assemble] manifest v${pkgVersion} (+skills decl), ${manifest.contracts?.tools?.length ?? 0} contract tools`,
94
+ );
79
95
  }
80
96
 
81
97
  // 3. The connector, bundled — this is what flips the supervisor's auto-start.
@@ -10,6 +10,13 @@ import { type Wave9LiveSymbolOwnershipCheck } from '../wave9/live-symbol-ownersh
10
10
  import { type Wave9LiveResidualProtectionResult } from '../wave9/live-residual-protection.js';
11
11
  import { type Wave9PaperAdmissionGuard } from '../wave9/paper-admission-guard.js';
12
12
  type Wave9LiveResidualProtector = (adapter: IExchangeAdapter, ledger: Wave9LiveExecutionLedger, candidateId: string, symbol: string) => Promise<Wave9LiveResidualProtectionResult>;
13
+ /** Venue-aware idempotency cid for the live entry stash. HL client-order-ids
14
+ * are 128-bit hex (0x + 32 hex chars) — a dashed randomUUID() fails
15
+ * hl-private's cloid validation and the ENTRY IS REJECTED before reaching
16
+ * the exchange (found live by the go-live rehearsal's first dust trade).
17
+ * buildHlOrderCloid() mints the 0x0d… ORDER prefix, which the bracket parser
18
+ * deliberately never recognises (cross-scheme cancels are destructive). */
19
+ export declare function mintEntryStashCid(adapter: IExchangeAdapter): string;
13
20
  export declare function createOrderTool(args: {
14
21
  symbol: string;
15
22
  side: string;
@@ -11,6 +11,8 @@ import { onCreateOrderFilled } from '../ingest/position-auto-capture.js';
11
11
  import { wave9ClientOrderId, } from '../wave9/live-execution-ledger.js';
12
12
  import { inspectWave9LiveSymbolOwnership, } from '../wave9/live-symbol-ownership.js';
13
13
  import { confirmWave9LivePositionFlat } from '../wave9/live-position-confirmation.js';
14
+ import { HyperliquidLiveAdapter } from '../venues/hyperliquid/hl-live-adapter.js';
15
+ import { buildHlOrderCloid } from '../venues/hyperliquid/hl-cloid.js';
14
16
  import { reprotectWave9LiveResidual, } from '../wave9/live-residual-protection.js';
15
17
  import { captureWave9PaperAccountSnapshot, isWave9ManagedPosition, WAVE9_BUNDLE_SETUP_TYPE, } from '../wave9/paper-admission-guard.js';
16
18
  function currentWave9ExecutionMode(deps) {
@@ -231,6 +233,15 @@ function candidateBoundFoundOrder(resolution, clientOrderId, symbol, side) {
231
233
  return undefined;
232
234
  return order;
233
235
  }
236
+ /** Venue-aware idempotency cid for the live entry stash. HL client-order-ids
237
+ * are 128-bit hex (0x + 32 hex chars) — a dashed randomUUID() fails
238
+ * hl-private's cloid validation and the ENTRY IS REJECTED before reaching
239
+ * the exchange (found live by the go-live rehearsal's first dust trade).
240
+ * buildHlOrderCloid() mints the 0x0d… ORDER prefix, which the bracket parser
241
+ * deliberately never recognises (cross-scheme cancels are destructive). */
242
+ export function mintEntryStashCid(adapter) {
243
+ return adapter instanceof HyperliquidLiveAdapter ? buildHlOrderCloid() : randomUUID();
244
+ }
234
245
  async function resolveWave9EntryCid(adapter, clientOrderId, symbol) {
235
246
  if (!adapter.resolveOrderByClientId) {
236
247
  return { status: 'unknown', detail: 'deterministic client-order resolver is unavailable' };
@@ -1133,7 +1144,7 @@ export async function createOrderTool(args, deps) {
1133
1144
  ? wave9ClientOrderId(args.candidate_id)
1134
1145
  : undefined;
1135
1146
  if (deps.autoCapture?.pendingEntries && metadata) {
1136
- stashCid ??= randomUUID();
1147
+ stashCid ??= mintEntryStashCid(deps.adapter);
1137
1148
  deps.autoCapture.pendingEntries.put({
1138
1149
  orderId: stashCid,
1139
1150
  clientOrderId: stashCid,
@@ -9,6 +9,7 @@
9
9
  // because the caller chain is currently controlled end-to-end by the skill.
10
10
  import { readPluginConfig } from '../config/plugin-config-io.js';
11
11
  import { validateModeTransition, modeRequiresCredentials } from '../onboarding/mode-ladder.js';
12
+ import { parseVenue } from '../venues/registry.js';
12
13
  import { logger } from '../logger.js';
13
14
  import { recordModeTransition } from '../audit/mode-transition-audit.js';
14
15
  const TAG = 'set-trading-mode';
@@ -49,12 +50,26 @@ export async function setTradingModeTool(args, deps) {
49
50
  readiness: deps.runtime.adapter.readiness,
50
51
  };
51
52
  }
52
- // 3. Credential requirement check.
53
+ // 3. Credential requirement check — PER VENUE (issue #217: this gate was
54
+ // Binance-only, so the go-live flip on the hyperliquid venue was rejected
55
+ // despite valid walletAddress+agentPrivateKey in plugin-config).
53
56
  let exchange = null;
57
+ let hlCredentials = null;
58
+ let venue = 'binance';
54
59
  if (modeRequiresCredentials(target)) {
55
60
  try {
56
61
  const cfg = readPluginConfig(deps.configPath);
57
- if (cfg.exchange?.apiKey && cfg.exchange?.secret) {
62
+ venue = parseVenue(cfg.exchange?.venue).venue;
63
+ if (venue === 'hyperliquid') {
64
+ if (cfg.exchange?.walletAddress && cfg.exchange?.agentPrivateKey) {
65
+ hlCredentials = {
66
+ walletAddress: String(cfg.exchange.walletAddress),
67
+ agentPrivateKey: String(cfg.exchange.agentPrivateKey),
68
+ testnet: cfg.exchange.testnet === true,
69
+ };
70
+ }
71
+ }
72
+ else if (cfg.exchange?.apiKey && cfg.exchange?.secret) {
58
73
  exchange = {
59
74
  apiKey: cfg.exchange.apiKey,
60
75
  secret: cfg.exchange.secret,
@@ -74,11 +89,13 @@ export async function setTradingModeTool(args, deps) {
74
89
  reason: 'config_read_error',
75
90
  };
76
91
  }
77
- if (!exchange) {
92
+ if (!exchange && !hlCredentials) {
78
93
  recordModeTransition({ previousMode, targetMode: target, acknowledged, ok: false, reason: 'missing_credentials' });
79
94
  return {
80
95
  ok: false,
81
- message: `${target} mode requires exchange credentials. Run set_exchange_credentials first.`,
96
+ message: venue === 'hyperliquid'
97
+ ? `${target} mode on Hyperliquid requires exchange.walletAddress (master) + exchange.agentPrivateKey (agent wallet) in plugin-config.`
98
+ : `${target} mode requires exchange credentials. Run set_exchange_credentials first.`,
82
99
  previousMode,
83
100
  mode: previousMode,
84
101
  readiness: deps.runtime.adapter.readiness,
@@ -116,14 +133,14 @@ export async function setTradingModeTool(args, deps) {
116
133
  };
117
134
  }
118
135
  // 6. Swap the adapter.
119
- await deps.runtime.reconnect({ mode: target, exchange }, { adapterDeps: deps.adapterDeps });
136
+ await deps.runtime.reconnect({ mode: target, exchange, venue, hlCredentials }, { adapterDeps: deps.adapterDeps });
120
137
  logger.info(TAG, `Transitioned ${previousMode} → ${target}`);
121
138
  recordModeTransition({
122
139
  previousMode,
123
140
  targetMode: target,
124
141
  acknowledged,
125
142
  ok: true,
126
- testnet: exchange?.testnet,
143
+ testnet: exchange?.testnet ?? hlCredentials?.testnet,
127
144
  });
128
145
  return {
129
146
  ok: true,