@reefclaw/connect 0.1.33 → 0.1.34
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.
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PluginRuntime } from '../onboarding/runtime.js';
|
|
2
2
|
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
3
3
|
import type { TradingMode } from '../types.js';
|
|
4
|
+
import type { HlCredentials } from '../venues/hyperliquid/hl-private.js';
|
|
5
|
+
import { type HlAgentApprovalVerdict } from '../venues/hyperliquid/hl-agent-wallet.js';
|
|
4
6
|
import { type VenueId } from '../venues/registry.js';
|
|
5
7
|
export interface SetTradingModeArgs {
|
|
6
8
|
mode: TradingMode;
|
|
@@ -27,6 +29,10 @@ export interface SetTradingModeDeps {
|
|
|
27
29
|
* wired to the boot venue and a mixed-venue runtime is not a valid state.
|
|
28
30
|
* Absent (legacy tests) → guard skipped. */
|
|
29
31
|
bootVenue?: VenueId;
|
|
32
|
+
/** Injectable approval check (tests). DEFAULTS TO THE REAL ONE — unlike
|
|
33
|
+
* bootVenue this gate is on unless explicitly stubbed, so production
|
|
34
|
+
* wiring can never forget it. */
|
|
35
|
+
hlApprovalCheck?: (creds: HlCredentials) => Promise<HlAgentApprovalVerdict>;
|
|
30
36
|
/** Override the config file path — tests use this. */
|
|
31
37
|
configPath?: string;
|
|
32
38
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// by design: the check lives once, at registration, for all operator tools.
|
|
13
13
|
import { readPluginConfig } from '../config/plugin-config-io.js';
|
|
14
14
|
import { validateModeTransition, modeRequiresCredentials } from '../onboarding/mode-ladder.js';
|
|
15
|
+
import { checkHlAgentApproval, } from '../venues/hyperliquid/hl-agent-wallet.js';
|
|
15
16
|
import { parseVenue } from '../venues/registry.js';
|
|
16
17
|
import { logger } from '../logger.js';
|
|
17
18
|
import { recordModeTransition } from '../audit/mode-transition-audit.js';
|
|
@@ -133,6 +134,48 @@ export async function setTradingModeTool(args, deps) {
|
|
|
133
134
|
reason: 'acknowledgment_required',
|
|
134
135
|
};
|
|
135
136
|
}
|
|
137
|
+
// 4.5. HL agent-approval preflight (E2E audit 2026-08-11 #7). An agent key
|
|
138
|
+
// that is not in the master account's extraAgents list — or whose approval
|
|
139
|
+
// expired (they last ≤180 days) — signs orders Hyperliquid rejects one by
|
|
140
|
+
// one: the flip would "succeed", the dashboard would look healthy, and
|
|
141
|
+
// every order would bounce. Refuse ONLY on a definitive negative from the
|
|
142
|
+
// exchange; derivation/network failures fail OPEN with a warning (infra
|
|
143
|
+
// must never block a deliberate operator action — same rule as the
|
|
144
|
+
// credential-entry guards).
|
|
145
|
+
let preflightWarnings = [];
|
|
146
|
+
if (venue === 'hyperliquid' && hlCredentials) {
|
|
147
|
+
let verdict;
|
|
148
|
+
try {
|
|
149
|
+
verdict = await (deps.hlApprovalCheck ?? checkHlAgentApproval)(hlCredentials);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
verdict = {
|
|
153
|
+
approved: null,
|
|
154
|
+
validUntil: null,
|
|
155
|
+
warnings: [
|
|
156
|
+
`Approval preflight errored (${err instanceof Error ? err.message : String(err)}) — approval not verified.`,
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (verdict.approved === false) {
|
|
161
|
+
recordModeTransition({ previousMode, targetMode: target, acknowledged, ok: false, reason: 'hl_agent_not_approved' });
|
|
162
|
+
const why = verdict.warnings.length > 0 ? ` ${verdict.warnings.join(' ')}` : '';
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
message: `Hyperliquid reports this agent wallet is NOT approved to trade for ${hlCredentials.walletAddress}.` +
|
|
166
|
+
`${why} Every live order would be rejected. Approve it first: Dashboard → Settings → ` +
|
|
167
|
+
`Exchange connection → Hyperliquid (the guided setup signs one approveAgent transaction), ` +
|
|
168
|
+
`then retry ${target}.`,
|
|
169
|
+
previousMode,
|
|
170
|
+
mode: previousMode,
|
|
171
|
+
readiness: deps.runtime.adapter.readiness,
|
|
172
|
+
reason: 'hl_agent_not_approved',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
preflightWarnings = verdict.warnings;
|
|
176
|
+
for (const w of preflightWarnings)
|
|
177
|
+
logger.warn(TAG, `HL preflight: ${w}`);
|
|
178
|
+
}
|
|
136
179
|
// 5. Persist the new mode.
|
|
137
180
|
try {
|
|
138
181
|
const { updatePluginConfig } = await import('../config/plugin-config-io.js');
|
|
@@ -162,7 +205,11 @@ export async function setTradingModeTool(args, deps) {
|
|
|
162
205
|
});
|
|
163
206
|
return {
|
|
164
207
|
ok: true,
|
|
165
|
-
|
|
208
|
+
// Preflight warnings (near-expiry approval, zero balance, unverifiable
|
|
209
|
+
// approval) ride the success message — the operator just took a real-money
|
|
210
|
+
// action and these are the things to fix before the first order.
|
|
211
|
+
message: `Trading mode changed from ${previousMode} to ${target}` +
|
|
212
|
+
(preflightWarnings.length > 0 ? `. Note: ${preflightWarnings.join(' ')}` : ''),
|
|
166
213
|
previousMode,
|
|
167
214
|
mode: deps.runtime.mode,
|
|
168
215
|
readiness: deps.runtime.adapter.readiness,
|
|
@@ -27,3 +27,29 @@ export declare function __resetKeccakCacheForTests(): void;
|
|
|
27
27
|
export declare function deriveAddressFromPrivateKey(privateKey: string): Promise<DeriveAddressResult>;
|
|
28
28
|
/** Case-insensitive address equality (addresses may arrive checksummed). */
|
|
29
29
|
export declare function sameAddress(a: string, b: string): boolean;
|
|
30
|
+
/** One-call approval verdict for the STORED credentials — derivation + the
|
|
31
|
+
* unsigned preflight, packaged for the live-flip gate in set_trading_mode
|
|
32
|
+
* (E2E audit 2026-08-11 #7: an unapproved/expired agent key reached LIVE
|
|
33
|
+
* with a healthy-looking dashboard and every order rejected).
|
|
34
|
+
*
|
|
35
|
+
* Lives HERE, not in hl-preflight.ts, because it takes the PRIVATE KEY —
|
|
36
|
+
* hl-preflight's module boundary is addresses-only by design.
|
|
37
|
+
*
|
|
38
|
+
* Verdict semantics (the caller refuses ONLY on `approved === false`):
|
|
39
|
+
* false = the exchange answered definitively — the agent address is not in
|
|
40
|
+
* extraAgents, or its approval has expired. Real money would be
|
|
41
|
+
* un-tradeable; block the flip.
|
|
42
|
+
* null = could not verify (derivation unavailable, HL unreachable,
|
|
43
|
+
* extraAgents unreadable). Fail OPEN — infra must never block a
|
|
44
|
+
* deliberate operator action; the warning travels instead.
|
|
45
|
+
* true = approved (warnings may still carry a near-expiry nudge). */
|
|
46
|
+
export interface HlAgentApprovalVerdict {
|
|
47
|
+
approved: boolean | null;
|
|
48
|
+
validUntil: number | null;
|
|
49
|
+
warnings: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function checkHlAgentApproval(creds: {
|
|
52
|
+
walletAddress: string;
|
|
53
|
+
agentPrivateKey: string;
|
|
54
|
+
testnet?: boolean;
|
|
55
|
+
}, fetchImpl?: typeof fetch): Promise<HlAgentApprovalVerdict>;
|
|
@@ -116,3 +116,35 @@ export async function deriveAddressFromPrivateKey(privateKey) {
|
|
|
116
116
|
export function sameAddress(a, b) {
|
|
117
117
|
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
118
118
|
}
|
|
119
|
+
export async function checkHlAgentApproval(creds, fetchImpl) {
|
|
120
|
+
const derived = await deriveAddressFromPrivateKey(creds.agentPrivateKey);
|
|
121
|
+
if (!derived.ok) {
|
|
122
|
+
return {
|
|
123
|
+
approved: null,
|
|
124
|
+
validUntil: null,
|
|
125
|
+
warnings: [
|
|
126
|
+
derived.reason === 'invalid_key'
|
|
127
|
+
? 'The stored agent key looks malformed — approval could not be verified.'
|
|
128
|
+
: 'Could not derive the agent wallet address on this host — approval not verified.',
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const { hlPreflight } = await import('./hl-preflight.js');
|
|
133
|
+
const pre = await hlPreflight({
|
|
134
|
+
walletAddress: creds.walletAddress,
|
|
135
|
+
agentAddress: derived.address,
|
|
136
|
+
testnet: creds.testnet === true,
|
|
137
|
+
fetchImpl,
|
|
138
|
+
});
|
|
139
|
+
if (!pre.reachable) {
|
|
140
|
+
return {
|
|
141
|
+
approved: null,
|
|
142
|
+
validUntil: null,
|
|
143
|
+
warnings: [
|
|
144
|
+
`Could not reach Hyperliquid to verify the agent approval (${pre.unreachableError ?? 'unknown error'}).`,
|
|
145
|
+
...pre.warnings,
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { approved: pre.agentApproved, validUntil: pre.agentValidUntil, warnings: pre.warnings };
|
|
150
|
+
}
|