@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 +9 -2
- package/onboarding/runtime.d.ts +12 -0
- package/onboarding/runtime.js +33 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/assemble.mjs +18 -2
- package/tools/create-order.d.ts +7 -0
- package/tools/create-order.js +12 -1
- package/tools/set-trading-mode.js +23 -6
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
|
-
|
|
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
|
|
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
|
}
|
package/onboarding/runtime.d.ts
CHANGED
|
@@ -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;
|
package/onboarding/runtime.js
CHANGED
|
@@ -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
|
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "reefclaw-paper-trading",
|
|
3
3
|
"name": "ReefClaw Paper Trading",
|
|
4
|
-
"version": "0.1.
|
|
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.
|
|
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",
|
package/scripts/assemble.mjs
CHANGED
|
@@ -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(
|
|
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.
|
package/tools/create-order.d.ts
CHANGED
|
@@ -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;
|
package/tools/create-order.js
CHANGED
|
@@ -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 ??=
|
|
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
|
-
|
|
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:
|
|
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,
|