@reefclaw/openclaw-plugin 0.1.20 → 0.1.22

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.
Files changed (37) hide show
  1. package/bridge/bridge.d.ts +10 -0
  2. package/bridge/bridge.js +167 -23
  3. package/bridge/gateway/tool-discovery.d.ts +1 -1
  4. package/bridge/gateway/tool-discovery.js +11 -0
  5. package/bridge/provider.d.ts +85 -6
  6. package/bridge/providers/gateway.d.ts +13 -3
  7. package/bridge/providers/gateway.js +18 -5
  8. package/bridge/providers/onboarding-commands.d.ts +38 -7
  9. package/bridge/providers/onboarding-commands.js +113 -13
  10. package/bridge/types.d.ts +1 -1
  11. package/bridge/types.js +5 -0
  12. package/ccxt/binance-private.js +6 -0
  13. package/config/plugin-config-io.d.ts +26 -0
  14. package/config/plugin-config-io.js +47 -0
  15. package/config/tool-gate.js +4 -0
  16. package/index.js +79 -12
  17. package/ingest/readiness-reporter.js +25 -1
  18. package/openclaw.plugin.json +3 -1
  19. package/package.json +1 -1
  20. package/security/sealed-credentials.d.ts +38 -0
  21. package/security/sealed-credentials.js +180 -0
  22. package/tools/clear-exchange-credentials.js +16 -3
  23. package/tools/hl-agent-wallet-status.d.ts +29 -0
  24. package/tools/hl-agent-wallet-status.js +100 -0
  25. package/tools/hl-provision-agent-wallet.d.ts +43 -0
  26. package/tools/hl-provision-agent-wallet.js +173 -0
  27. package/tools/set-exchange-credentials.d.ts +42 -3
  28. package/tools/set-exchange-credentials.js +208 -27
  29. package/tools/set-trading-mode.d.ts +7 -0
  30. package/tools/set-trading-mode.js +15 -0
  31. package/tools/test-exchange-credentials.d.ts +30 -3
  32. package/tools/test-exchange-credentials.js +173 -35
  33. package/types.d.ts +16 -11
  34. package/venues/hyperliquid/hl-agent-wallet.d.ts +29 -0
  35. package/venues/hyperliquid/hl-agent-wallet.js +118 -0
  36. package/venues/hyperliquid/hl-preflight.d.ts +28 -0
  37. package/venues/hyperliquid/hl-preflight.js +116 -0
package/types.d.ts CHANGED
@@ -111,24 +111,29 @@ export declare const DEFAULT_CONFIG: PluginConfig;
111
111
  * `exchange` block (openclaw.json legacy fallback). Which fields matter
112
112
  * depends on `venue` (docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.2):
113
113
  *
114
- * binance (default, and the only live-supported venue until Phase 3) —
115
- * apiKey + secret (HMAC pair) are required; walletAddress/agentPrivateKey
116
- * are ignored.
114
+ * binance (default) apiKey + secret (HMAC pair) are required;
115
+ * walletAddress/agentPrivateKey are ignored.
117
116
  * hyperliquid — walletAddress (MASTER wallet address, 0x…, used for
118
117
  * queries; never its private key) + agentPrivateKey (an approved
119
118
  * agent/API wallet's key — signs orders, cannot withdraw). apiKey/secret
120
- * are meaningless. Boot falls back to PAPER for this venue until the
121
- * Phase 3 adapter ships.
119
+ * are absent an HL block never carries a Binance HMAC pair.
122
120
  *
123
- * apiKey/secret stay required at the type level because every constructed
124
- * instance today feeds BinancePrivateApi; the on-disk JSON is parsed, not
125
- * type-constructed, so an HL block without them is readable. Phase 3
126
- * restructures this into a per-venue discriminated union. */
121
+ * apiKey/secret are optional at the type level because the on-disk shape is
122
+ * per-venue: every Binance consumer (index.ts boot, set_trading_mode,
123
+ * set_exchange_credentials) truthiness-guards the pair before constructing a
124
+ * BinancePrivateApi, so an HL block without them is both readable AND
125
+ * writable. */
127
126
  export interface ExchangeConfig {
128
127
  venue?: 'binance' | 'hyperliquid';
129
- apiKey: string;
130
- secret: string;
128
+ apiKey?: string;
129
+ secret?: string;
131
130
  testnet?: boolean;
132
131
  walletAddress?: string;
133
132
  agentPrivateKey?: string;
133
+ /** Per-venue testnet memo. `testnet` above is the ACTIVE venue's flag (what
134
+ * boot reads); this remembers the OTHER venue's setting so switching back
135
+ * restores it instead of inheriting the flag from whichever venue was
136
+ * configured last. Traders run both venues and switch between them — a
137
+ * switch must never lose a configured setting. */
138
+ testnetByVenue?: Partial<Record<'binance' | 'hyperliquid', boolean>>;
134
139
  }
@@ -0,0 +1,29 @@
1
+ /** Generate a fresh secp256k1 private key (0x-prefixed, 64 hex chars) from
2
+ * CSPRNG bytes, rejection-sampled into the valid scalar range. This is how
3
+ * the box mints its own HL agent wallet — the key is BORN here and never
4
+ * leaves the machine (the caller persists it to plugin-config and surfaces
5
+ * only the derived PUBLIC address). */
6
+ export declare function generateAgentPrivateKey(): string;
7
+ /** Master/agent wallet address: 0x + 40 hex chars. */
8
+ export declare function isHexAddress(value: unknown): value is string;
9
+ /** Agent private key: 32 bytes of hex, 0x prefix optional (the HL app shows it
10
+ * with the prefix; some wallets export without). */
11
+ export declare function isHexPrivateKey(value: unknown): value is string;
12
+ /** Canonical stored form: trimmed, 0x-prefixed, original casing preserved. */
13
+ export declare function normalizeHexPrivateKey(value: string): string;
14
+ export type DeriveAddressResult = {
15
+ ok: true;
16
+ address: string;
17
+ } | {
18
+ ok: false;
19
+ reason: 'invalid_key' | 'unavailable';
20
+ };
21
+ /** Test seam: reset the memoized keccak loader (unit tests only). */
22
+ export declare function __resetKeccakCacheForTests(): void;
23
+ /** Derive the Ethereum address a private key controls (lowercase 0x…40 hex).
24
+ * 'invalid_key' = the key is malformed or outside the secp256k1 range;
25
+ * 'unavailable' = the keccak dependency could not be loaded (callers must
26
+ * fail OPEN — skip the check, never block credential entry on infra). */
27
+ export declare function deriveAddressFromPrivateKey(privateKey: string): Promise<DeriveAddressResult>;
28
+ /** Case-insensitive address equality (addresses may arrive checksummed). */
29
+ export declare function sameAddress(a: string, b: string): boolean;
@@ -0,0 +1,118 @@
1
+ // Hyperliquid agent-wallet helpers — format validation + address derivation.
2
+ //
3
+ // Why derivation exists at all: the single most catastrophic mistake a
4
+ // non-technical trader can make in the HL onboarding form is pasting their
5
+ // MASTER wallet's private key where the AGENT key belongs (the master key can
6
+ // withdraw everything; the agent key can only trade). The form cannot tell the
7
+ // two apart — both are 32-byte hex — but the box can: derive the address from
8
+ // the pasted key and refuse when it equals the master walletAddress. The same
9
+ // derived address also lets test_exchange_credentials verify the agent is
10
+ // actually APPROVED for the master account (POST /info extraAgents) before the
11
+ // user goes live.
12
+ //
13
+ // Derivation = secp256k1 pubkey (node:crypto createECDH — stable Node API) +
14
+ // keccak-256 of the 64-byte public key, last 20 bytes (the Ethereum address
15
+ // rule). Node has no keccak (its sha3-256 is NIST-padded, a different hash),
16
+ // so keccak_256 comes from ccxt's vendored noble-hashes — ccxt is pinned at
17
+ // 4.5.37 and the deep path is guarded by a known-vector unit test
18
+ // (hl-agent-wallet.test.ts), so a ccxt bump that moves the file fails loudly.
19
+ // If the import ever breaks at runtime we degrade gracefully: derivation
20
+ // reports 'unavailable' and callers skip the address checks rather than
21
+ // blocking credential entry.
22
+ //
23
+ // SECURITY: this module handles the raw agent private key in memory only —
24
+ // it never logs it, never persists it, and never puts it in a return value.
25
+ import { createECDH, randomBytes } from 'node:crypto';
26
+ import { createRequire } from 'node:module';
27
+ import { pathToFileURL } from 'node:url';
28
+ import path from 'node:path';
29
+ /** secp256k1 group order n — a private key must be in [1, n-1]. */
30
+ const SECP256K1_ORDER = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');
31
+ /** Generate a fresh secp256k1 private key (0x-prefixed, 64 hex chars) from
32
+ * CSPRNG bytes, rejection-sampled into the valid scalar range. This is how
33
+ * the box mints its own HL agent wallet — the key is BORN here and never
34
+ * leaves the machine (the caller persists it to plugin-config and surfaces
35
+ * only the derived PUBLIC address). */
36
+ export function generateAgentPrivateKey() {
37
+ // Rejection probability per draw is ~2^-128 (order ≈ 2^256) — the loop is
38
+ // for correctness, not because retries are expected.
39
+ for (;;) {
40
+ const candidate = randomBytes(32);
41
+ const scalar = BigInt(`0x${candidate.toString('hex')}`);
42
+ if (scalar > 0n && scalar < SECP256K1_ORDER) {
43
+ return `0x${candidate.toString('hex')}`;
44
+ }
45
+ }
46
+ }
47
+ /** Master/agent wallet address: 0x + 40 hex chars. */
48
+ export function isHexAddress(value) {
49
+ return typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value.trim());
50
+ }
51
+ /** Agent private key: 32 bytes of hex, 0x prefix optional (the HL app shows it
52
+ * with the prefix; some wallets export without). */
53
+ export function isHexPrivateKey(value) {
54
+ return typeof value === 'string' && /^(0x)?[0-9a-fA-F]{64}$/.test(value.trim());
55
+ }
56
+ /** Canonical stored form: trimmed, 0x-prefixed, original casing preserved. */
57
+ export function normalizeHexPrivateKey(value) {
58
+ const trimmed = value.trim();
59
+ return trimmed.startsWith('0x') || trimmed.startsWith('0X') ? `0x${trimmed.slice(2)}` : `0x${trimmed}`;
60
+ }
61
+ let keccakPromise = null;
62
+ /** Load keccak_256 from ccxt's vendored noble-hashes. The package's `exports`
63
+ * map only exposes ".", so we resolve the package root from the main entry and
64
+ * import the file by path (file URLs bypass the exports map). Returns null on
65
+ * any failure — derivation then reports 'unavailable'. */
66
+ function loadKeccak() {
67
+ if (!keccakPromise) {
68
+ keccakPromise = (async () => {
69
+ try {
70
+ const req = createRequire(import.meta.url);
71
+ const entry = req.resolve('ccxt');
72
+ const marker = `${path.sep}ccxt${path.sep}`;
73
+ const idx = entry.lastIndexOf(marker);
74
+ if (idx === -1)
75
+ return null;
76
+ const root = entry.slice(0, idx + marker.length - 1);
77
+ const sha3 = path.join(root, 'js', 'src', 'static_dependencies', 'noble-hashes', 'sha3.js');
78
+ const mod = (await import(pathToFileURL(sha3).href));
79
+ return typeof mod.keccak_256 === 'function' ? mod.keccak_256 : null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ })();
85
+ }
86
+ return keccakPromise;
87
+ }
88
+ /** Test seam: reset the memoized keccak loader (unit tests only). */
89
+ export function __resetKeccakCacheForTests() {
90
+ keccakPromise = null;
91
+ }
92
+ /** Derive the Ethereum address a private key controls (lowercase 0x…40 hex).
93
+ * 'invalid_key' = the key is malformed or outside the secp256k1 range;
94
+ * 'unavailable' = the keccak dependency could not be loaded (callers must
95
+ * fail OPEN — skip the check, never block credential entry on infra). */
96
+ export async function deriveAddressFromPrivateKey(privateKey) {
97
+ if (!isHexPrivateKey(privateKey))
98
+ return { ok: false, reason: 'invalid_key' };
99
+ const keccak = await loadKeccak();
100
+ if (!keccak)
101
+ return { ok: false, reason: 'unavailable' };
102
+ try {
103
+ const ecdh = createECDH('secp256k1');
104
+ ecdh.setPrivateKey(Buffer.from(normalizeHexPrivateKey(privateKey).slice(2), 'hex'));
105
+ const uncompressed = ecdh.getPublicKey(null, 'uncompressed'); // 0x04 || X || Y (65 bytes)
106
+ const digest = keccak(Uint8Array.from(uncompressed.subarray(1)));
107
+ const address = `0x${Buffer.from(digest.subarray(digest.length - 20)).toString('hex')}`;
108
+ return { ok: true, address };
109
+ }
110
+ catch {
111
+ // createECDH throws on out-of-range keys — malformed input, not infra.
112
+ return { ok: false, reason: 'invalid_key' };
113
+ }
114
+ }
115
+ /** Case-insensitive address equality (addresses may arrive checksummed). */
116
+ export function sameAddress(a, b) {
117
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
118
+ }
@@ -0,0 +1,28 @@
1
+ /** Approvals expiring inside this window get a renewal warning — long enough
2
+ * to act on, short enough not to nag for months. */
3
+ export declare const HL_EXPIRY_WARN_MS: number;
4
+ export interface HlPreflightInput {
5
+ walletAddress: string;
6
+ /** Derived agent address to check approval for; null = skip the check
7
+ * (derivation unavailable). */
8
+ agentAddress: string | null;
9
+ testnet: boolean;
10
+ /** Injectable for tests; production uses global fetch. */
11
+ fetchImpl?: typeof fetch;
12
+ }
13
+ export interface HlPreflightOutcome {
14
+ /** clearinghouseState answered and parsed — the master account is queryable. */
15
+ reachable: boolean;
16
+ /** Set when !reachable — the raw error string for the caller's message. */
17
+ unreachableError?: string;
18
+ /** Perp equity + spot USDC; null when unreachable. */
19
+ balanceUSDC: number | null;
20
+ /** true/false = extraAgents answered definitively; null = could not check. */
21
+ agentApproved: boolean | null;
22
+ /** Approval expiry (epoch ms) when the exchange reports one. */
23
+ agentValidUntil: number | null;
24
+ warnings: string[];
25
+ }
26
+ export declare function hlApiBase(testnet: boolean): string;
27
+ export declare function hlInfo(fetchImpl: typeof fetch, testnet: boolean, body: Record<string, unknown>): Promise<unknown>;
28
+ export declare function hlPreflight(input: HlPreflightInput): Promise<HlPreflightOutcome>;
@@ -0,0 +1,116 @@
1
+ // Hyperliquid credential preflight — the UNSIGNED verification core shared by
2
+ // test_exchange_credentials (candidate creds) and hl_agent_wallet_status
3
+ // (stored creds). Deliberately takes ADDRESSES ONLY — this module never sees a
4
+ // private key, so it stays outside the credential-holder set.
5
+ //
6
+ // Checks (all keyless POST /info reads — the master address is the query key):
7
+ // 1. clearinghouseState(master) → perp equity. Failure here = "unreachable";
8
+ // callers hard-fail with the error string.
9
+ // 2. spotClearinghouseState(master) → spot USDC (the unified account
10
+ // collateralizes perps from spot — verified live on testnet 2026-07-12).
11
+ // Cosmetic: failure is silently skipped, perp equity alone still answers.
12
+ // 3. extraAgents(master) → is `agentAddress` in the approved list, and when
13
+ // does the approval expire? Shape [{name,address,validUntil(ms)}] —
14
+ // verified vs Chainstack/QuickNode HL API references 2026-08-01 (the
15
+ // official gitbook omits this info type). A definitive miss is the
16
+ // caller's hard fail; an endpoint error degrades to a warning
17
+ // (fail-open on infra, closed on an explicit "not approved").
18
+ import { sameAddress } from './hl-agent-wallet.js';
19
+ /** Approvals expiring inside this window get a renewal warning — long enough
20
+ * to act on, short enough not to nag for months. */
21
+ export const HL_EXPIRY_WARN_MS = 14 * 24 * 60 * 60 * 1000;
22
+ export function hlApiBase(testnet) {
23
+ return testnet ? 'https://api.hyperliquid-testnet.xyz' : 'https://api.hyperliquid.xyz';
24
+ }
25
+ export async function hlInfo(fetchImpl, testnet, body) {
26
+ const res = await fetchImpl(`${hlApiBase(testnet)}/info`, {
27
+ method: 'POST',
28
+ headers: { 'content-type': 'application/json' },
29
+ body: JSON.stringify(body),
30
+ signal: AbortSignal.timeout(15_000),
31
+ });
32
+ if (!res.ok)
33
+ throw new Error(`POST /info ${String(body.type)} → ${res.status}`);
34
+ return res.json();
35
+ }
36
+ function msg(err) {
37
+ return err instanceof Error ? err.message : String(err);
38
+ }
39
+ export async function hlPreflight(input) {
40
+ const fetchImpl = input.fetchImpl ?? fetch;
41
+ const { walletAddress, testnet } = input;
42
+ const warnings = [];
43
+ // 1+2. Master account state.
44
+ let balanceUSDC = null;
45
+ try {
46
+ const state = (await hlInfo(fetchImpl, testnet, {
47
+ type: 'clearinghouseState',
48
+ user: walletAddress,
49
+ }));
50
+ const perpEquity = Number(state?.marginSummary?.accountValue);
51
+ let total = Number.isFinite(perpEquity) ? perpEquity : 0;
52
+ try {
53
+ const spot = (await hlInfo(fetchImpl, testnet, {
54
+ type: 'spotClearinghouseState',
55
+ user: walletAddress,
56
+ }));
57
+ for (const b of spot?.balances ?? []) {
58
+ if (String(b?.coin) === 'USDC') {
59
+ const t = Number(b?.total);
60
+ if (Number.isFinite(t))
61
+ total += t;
62
+ }
63
+ }
64
+ }
65
+ catch {
66
+ // Spot read is cosmetic — perp equity alone is still a real answer.
67
+ }
68
+ balanceUSDC = total;
69
+ if (total <= 0) {
70
+ warnings.push('The master account holds no USDC yet — deposit/bridge USDC to Hyperliquid before going live.');
71
+ }
72
+ }
73
+ catch (e) {
74
+ return {
75
+ reachable: false,
76
+ unreachableError: msg(e),
77
+ balanceUSDC: null,
78
+ agentApproved: null,
79
+ agentValidUntil: null,
80
+ warnings,
81
+ };
82
+ }
83
+ // 3. Agent approval.
84
+ let agentApproved = null;
85
+ let agentValidUntil = null;
86
+ if (input.agentAddress) {
87
+ try {
88
+ const agents = (await hlInfo(fetchImpl, testnet, {
89
+ type: 'extraAgents',
90
+ user: walletAddress,
91
+ }));
92
+ if (Array.isArray(agents)) {
93
+ const match = agents.find((a) => typeof a?.address === 'string' && sameAddress(a.address, input.agentAddress));
94
+ agentApproved = match != null;
95
+ if (match && Number.isFinite(Number(match.validUntil))) {
96
+ agentValidUntil = Number(match.validUntil);
97
+ const remaining = agentValidUntil - Date.now();
98
+ if (remaining <= 0) {
99
+ agentApproved = false;
100
+ warnings.push('The agent wallet approval has EXPIRED — re-approve it in the Hyperliquid API page.');
101
+ }
102
+ else if (remaining < HL_EXPIRY_WARN_MS) {
103
+ warnings.push(`The agent wallet approval expires in ${Math.ceil(remaining / (24 * 60 * 60 * 1000))} day(s) — renew it soon (approvals last at most 180 days).`);
104
+ }
105
+ }
106
+ }
107
+ else {
108
+ warnings.push('Could not read the approved-agents list — approval not verified.');
109
+ }
110
+ }
111
+ catch {
112
+ warnings.push('Could not read the approved-agents list — approval not verified.');
113
+ }
114
+ }
115
+ return { reachable: true, balanceUSDC, agentApproved, agentValidUntil, warnings };
116
+ }