@jinn-network/client 0.1.1 → 0.1.2-canary.d6e72dfd
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 +14 -0
- package/CONTRIBUTING.md +123 -0
- package/README.md +210 -37
- package/deployments/deployment-claim-registry-baseSepolia.json +13 -0
- package/deployments/deployment-jinn-testnet-faucet-baseSepolia-fast.json +15 -0
- package/dist/adapters/claim-registry/abi.d.ts +127 -0
- package/dist/adapters/claim-registry/abi.js +93 -0
- package/dist/adapters/claim-registry/abi.js.map +1 -0
- package/dist/adapters/claim-registry/client.d.ts +89 -0
- package/dist/adapters/claim-registry/client.js +205 -0
- package/dist/adapters/claim-registry/client.js.map +1 -0
- package/dist/adapters/mech/adapter.d.ts +1 -0
- package/dist/adapters/mech/adapter.js +75 -41
- package/dist/adapters/mech/adapter.js.map +1 -1
- package/dist/adapters/mech/contracts.d.ts +2 -0
- package/dist/adapters/mech/contracts.js +57 -7
- package/dist/adapters/mech/contracts.js.map +1 -1
- package/dist/adapters/mech/ipfs.d.ts +8 -0
- package/dist/adapters/mech/ipfs.js +12 -0
- package/dist/adapters/mech/ipfs.js.map +1 -1
- package/dist/adapters/mech/types.d.ts +20 -46
- package/dist/adapters/mech/types.js +16 -35
- package/dist/adapters/mech/types.js.map +1 -1
- package/dist/api/gather-status.d.ts +1 -0
- package/dist/api/gather-status.js +33 -1
- package/dist/api/gather-status.js.map +1 -1
- package/dist/api/portfolio-v0-build.d.ts +81 -0
- package/dist/api/portfolio-v0-build.js +141 -0
- package/dist/api/portfolio-v0-build.js.map +1 -0
- package/dist/api/portfolio-v0-doctor.d.ts +37 -0
- package/dist/api/portfolio-v0-doctor.js +123 -0
- package/dist/api/portfolio-v0-doctor.js.map +1 -0
- package/dist/api/rewards-build.js +1 -1
- package/dist/api/rewards-build.js.map +1 -1
- package/dist/api/status-build.d.ts +7 -0
- package/dist/api/status-build.js +1 -0
- package/dist/api/status-build.js.map +1 -1
- package/dist/bin/jinn-mcp.d.ts +0 -12
- package/dist/bin/jinn-mcp.js +5 -14
- package/dist/bin/jinn-mcp.js.map +1 -1
- package/dist/build-meta.json +1 -1
- package/dist/cli/commands/auth.js +115 -25
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/bootstrap.js +1 -0
- package/dist/cli/commands/bootstrap.js.map +1 -1
- package/dist/cli/commands/doctor.js +130 -14
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/fleet-scale.js +1 -0
- package/dist/cli/commands/fleet-scale.js.map +1 -1
- package/dist/cli/commands/fund-requirements.js +2 -0
- package/dist/cli/commands/fund-requirements.js.map +1 -1
- package/dist/cli/commands/intents.d.ts +17 -0
- package/dist/cli/commands/intents.js +489 -0
- package/dist/cli/commands/intents.js.map +1 -0
- package/dist/cli/commands/keys-backup.js +13 -11
- package/dist/cli/commands/keys-backup.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.js +19 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/plugin-install.js +8 -4
- package/dist/cli/commands/plugin-install.js.map +1 -1
- package/dist/cli/commands/quickstart.js +60 -4
- package/dist/cli/commands/quickstart.js.map +1 -1
- package/dist/cli/commands/rewards.js +27 -1
- package/dist/cli/commands/rewards.js.map +1 -1
- package/dist/cli/commands/submit-intent.js +108 -5
- package/dist/cli/commands/submit-intent.js.map +1 -1
- package/dist/cli/commands/version.js +1 -0
- package/dist/cli/commands/version.js.map +1 -1
- package/dist/cli/deployment-digest.js +5 -0
- package/dist/cli/deployment-digest.js.map +1 -1
- package/dist/cli/execution-context.js +1 -0
- package/dist/cli/execution-context.js.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/intent-registry-access.d.ts +64 -0
- package/dist/cli/intent-registry-access.js +187 -0
- package/dist/cli/intent-registry-access.js.map +1 -0
- package/dist/cli/introspection-context.js +1 -0
- package/dist/cli/introspection-context.js.map +1 -1
- package/dist/cli/password.d.ts +21 -9
- package/dist/cli/password.js +45 -24
- package/dist/cli/password.js.map +1 -1
- package/dist/config.d.ts +110 -8
- package/dist/config.js +41 -12
- package/dist/config.js.map +1 -1
- package/dist/daemon/creator.d.ts +7 -1
- package/dist/daemon/creator.js +38 -3
- package/dist/daemon/creator.js.map +1 -1
- package/dist/daemon/daemon.d.ts +43 -0
- package/dist/daemon/daemon.js +87 -2
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/earning/bootstrap.d.ts +2 -1
- package/dist/earning/bootstrap.js +72 -4
- package/dist/earning/bootstrap.js.map +1 -1
- package/dist/earning/contracts.d.ts +10 -0
- package/dist/earning/contracts.js +24 -0
- package/dist/earning/contracts.js.map +1 -1
- package/dist/earning/jinn-rewards.d.ts +9 -0
- package/dist/earning/jinn-rewards.js +7 -0
- package/dist/earning/jinn-rewards.js.map +1 -1
- package/dist/intents/prediction-apy-v0-auto.d.ts +11 -0
- package/dist/intents/prediction-apy-v0-auto.js +46 -0
- package/dist/intents/prediction-apy-v0-auto.js.map +1 -0
- package/dist/intents/prediction-apy-v0-template.d.ts +8 -0
- package/dist/intents/prediction-apy-v0-template.js +22 -0
- package/dist/intents/prediction-apy-v0-template.js.map +1 -0
- package/dist/intents/prediction-v0-auto.d.ts +53 -0
- package/dist/intents/prediction-v0-auto.js +84 -0
- package/dist/intents/prediction-v0-auto.js.map +1 -0
- package/dist/intents/prediction-v0-template.d.ts +65 -0
- package/dist/intents/prediction-v0-template.js +125 -0
- package/dist/intents/prediction-v0-template.js.map +1 -0
- package/dist/main.js +149 -1
- package/dist/main.js.map +1 -1
- package/dist/mcp/operator-server.d.ts +1 -1
- package/dist/mcp/operator-server.js +1 -1
- package/dist/preflight/claude-auth.d.ts +12 -1
- package/dist/preflight/claude-auth.js +21 -3
- package/dist/preflight/claude-auth.js.map +1 -1
- package/dist/restorer/engine/canonical-json.d.ts +18 -0
- package/dist/restorer/engine/canonical-json.js +59 -0
- package/dist/restorer/engine/canonical-json.js.map +1 -0
- package/dist/restorer/engine/claim.d.ts +69 -0
- package/dist/restorer/engine/claim.js +104 -0
- package/dist/restorer/engine/claim.js.map +1 -0
- package/dist/restorer/engine/delivery.d.ts +52 -0
- package/dist/restorer/engine/delivery.js +63 -0
- package/dist/restorer/engine/delivery.js.map +1 -0
- package/dist/restorer/engine/engine.d.ts +203 -0
- package/dist/restorer/engine/engine.js +753 -0
- package/dist/restorer/engine/engine.js.map +1 -0
- package/dist/restorer/engine/manifest-assembly.d.ts +67 -0
- package/dist/restorer/engine/manifest-assembly.js +79 -0
- package/dist/restorer/engine/manifest-assembly.js.map +1 -0
- package/dist/restorer/engine/packaging.d.ts +87 -0
- package/dist/restorer/engine/packaging.js +350 -0
- package/dist/restorer/engine/packaging.js.map +1 -0
- package/dist/restorer/engine/persistence.d.ts +170 -0
- package/dist/restorer/engine/persistence.js +381 -0
- package/dist/restorer/engine/persistence.js.map +1 -0
- package/dist/restorer/engine/recovery.d.ts +22 -0
- package/dist/restorer/engine/recovery.js +24 -0
- package/dist/restorer/engine/recovery.js.map +1 -0
- package/dist/restorer/engine/registry.d.ts +62 -0
- package/dist/restorer/engine/registry.js +73 -0
- package/dist/restorer/engine/registry.js.map +1 -0
- package/dist/restorer/engine/signing.d.ts +30 -0
- package/dist/restorer/engine/signing.js +39 -0
- package/dist/restorer/engine/signing.js.map +1 -0
- package/dist/restorer/engine/state.d.ts +42 -0
- package/dist/restorer/engine/state.js +87 -0
- package/dist/restorer/engine/state.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/api-wallet.d.ts +64 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/api-wallet.js +96 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/api-wallet.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/index.d.ts +101 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/index.js +710 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/index.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/mcp-tools.d.ts +137 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/mcp-tools.js +865 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/mcp-tools.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/safety-rails.d.ts +74 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/safety-rails.js +74 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/safety-rails.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/session-orchestrator.d.ts +97 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/session-orchestrator.js +226 -0
- package/dist/restorer/impls/claude-mcp-hyperliquid/session-orchestrator.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/index.d.ts +43 -0
- package/dist/restorer/impls/claude-mcp-prediction/index.js +230 -0
- package/dist/restorer/impls/claude-mcp-prediction/index.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/mcp-tools.d.ts +38 -0
- package/dist/restorer/impls/claude-mcp-prediction/mcp-tools.js +135 -0
- package/dist/restorer/impls/claude-mcp-prediction/mcp-tools.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/prompt.d.ts +8 -0
- package/dist/restorer/impls/claude-mcp-prediction/prompt.js +54 -0
- package/dist/restorer/impls/claude-mcp-prediction/prompt.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/session-orchestrator.d.ts +36 -0
- package/dist/restorer/impls/claude-mcp-prediction/session-orchestrator.js +137 -0
- package/dist/restorer/impls/claude-mcp-prediction/session-orchestrator.js.map +1 -0
- package/dist/restorer/impls/claude-mcp-prediction/types.d.ts +82 -0
- package/dist/restorer/impls/claude-mcp-prediction/types.js +6 -0
- package/dist/restorer/impls/claude-mcp-prediction/types.js.map +1 -0
- package/dist/restorer/impls/legacy-claude/index.d.ts +45 -0
- package/dist/restorer/impls/legacy-claude/index.js +71 -0
- package/dist/restorer/impls/legacy-claude/index.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/canonical-metrics.d.ts +68 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/canonical-metrics.js +117 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/canonical-metrics.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/availability.d.ts +49 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/availability.js +91 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/availability.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/consistency.d.ts +78 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/consistency.js +274 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/consistency.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/eligibility.d.ts +23 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/eligibility.js +49 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/eligibility.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/integrity.d.ts +25 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/integrity.js +44 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/integrity.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/spec.d.ts +17 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/spec.js +43 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/checks/spec.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/index.d.ts +43 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/index.js +431 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/index.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/score.d.ts +21 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/score.js +32 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/score.js.map +1 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/types.d.ts +32 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/types.js +8 -0
- package/dist/restorer/impls/portfolio-v0-evaluator/types.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/index.d.ts +39 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/index.js +98 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/index.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/strategy.d.ts +2 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/strategy.js +7 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/strategy.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/types.d.ts +4 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/types.js +2 -0
- package/dist/restorer/impls/prediction-apy-v0-baseline/types.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/canonical-metrics.d.ts +2 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/canonical-metrics.js +7 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/canonical-metrics.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/index.d.ts +39 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/index.js +186 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/index.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/score.d.ts +9 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/score.js +20 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/score.js.map +1 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/types.d.ts +7 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/types.js +2 -0
- package/dist/restorer/impls/prediction-apy-v0-evaluator/types.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-baseline/index.d.ts +29 -0
- package/dist/restorer/impls/prediction-v0-baseline/index.js +94 -0
- package/dist/restorer/impls/prediction-v0-baseline/index.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-baseline/strategy.d.ts +8 -0
- package/dist/restorer/impls/prediction-v0-baseline/strategy.js +41 -0
- package/dist/restorer/impls/prediction-v0-baseline/strategy.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-baseline/types.d.ts +7 -0
- package/dist/restorer/impls/prediction-v0-baseline/types.js +2 -0
- package/dist/restorer/impls/prediction-v0-baseline/types.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/canonical-metrics.d.ts +20 -0
- package/dist/restorer/impls/prediction-v0-evaluator/canonical-metrics.js +66 -0
- package/dist/restorer/impls/prediction-v0-evaluator/canonical-metrics.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/availability.d.ts +9 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/availability.js +23 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/availability.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/eligibility.d.ts +3 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/eligibility.js +13 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/eligibility.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/integrity.d.ts +7 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/integrity.js +93 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/integrity.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/spec.d.ts +5 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/spec.js +20 -0
- package/dist/restorer/impls/prediction-v0-evaluator/checks/spec.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/index.d.ts +33 -0
- package/dist/restorer/impls/prediction-v0-evaluator/index.js +208 -0
- package/dist/restorer/impls/prediction-v0-evaluator/index.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/score.d.ts +8 -0
- package/dist/restorer/impls/prediction-v0-evaluator/score.js +15 -0
- package/dist/restorer/impls/prediction-v0-evaluator/score.js.map +1 -0
- package/dist/restorer/impls/prediction-v0-evaluator/types.d.ts +7 -0
- package/dist/restorer/impls/prediction-v0-evaluator/types.js +2 -0
- package/dist/restorer/impls/prediction-v0-evaluator/types.js.map +1 -0
- package/dist/restorer/types.d.ts +177 -0
- package/dist/restorer/types.js +7 -0
- package/dist/restorer/types.js.map +1 -0
- package/dist/store/store.d.ts +3 -1
- package/dist/store/store.js +3 -0
- package/dist/store/store.js.map +1 -1
- package/dist/types/desired-state.d.ts +53 -0
- package/dist/types/desired-state.js +20 -0
- package/dist/types/desired-state.js.map +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.js +4 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/portfolio.d.ts +1000 -0
- package/dist/types/portfolio.js +168 -0
- package/dist/types/portfolio.js.map +1 -0
- package/dist/types/prediction-apy.d.ts +919 -0
- package/dist/types/prediction-apy.js +121 -0
- package/dist/types/prediction-apy.js.map +1 -0
- package/dist/types/prediction.d.ts +925 -0
- package/dist/types/prediction.js +140 -0
- package/dist/types/prediction.js.map +1 -0
- package/dist/venues/aave-v3/addresses.d.ts +6 -0
- package/dist/venues/aave-v3/addresses.js +19 -0
- package/dist/venues/aave-v3/addresses.js.map +1 -0
- package/dist/venues/aave-v3/client.d.ts +81 -0
- package/dist/venues/aave-v3/client.js +97 -0
- package/dist/venues/aave-v3/client.js.map +1 -0
- package/dist/venues/chainlink/client.d.ts +99 -0
- package/dist/venues/chainlink/client.js +130 -0
- package/dist/venues/chainlink/client.js.map +1 -0
- package/dist/venues/chainlink/feeds.d.ts +8 -0
- package/dist/venues/chainlink/feeds.js +9 -0
- package/dist/venues/chainlink/feeds.js.map +1 -0
- package/dist/venues/hyperliquid/account-value.d.ts +30 -0
- package/dist/venues/hyperliquid/account-value.js +30 -0
- package/dist/venues/hyperliquid/account-value.js.map +1 -0
- package/dist/venues/hyperliquid/client.d.ts +63 -0
- package/dist/venues/hyperliquid/client.js +135 -0
- package/dist/venues/hyperliquid/client.js.map +1 -0
- package/dist/venues/hyperliquid/grid.d.ts +36 -0
- package/dist/venues/hyperliquid/grid.js +61 -0
- package/dist/venues/hyperliquid/grid.js.map +1 -0
- package/dist/venues/hyperliquid/types.d.ts +81 -0
- package/dist/venues/hyperliquid/types.js +8 -0
- package/dist/venues/hyperliquid/types.js.map +1 -0
- package/dist/withdraw/run-withdraw-plan.js +2 -0
- package/dist/withdraw/run-withdraw-plan.js.map +1 -1
- package/docker-compose.yml +44 -0
- package/package.json +12 -1
- package/skills/jinn-operator/SKILL.md +85 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool definitions for claude-mcp-hyperliquid — §8.2.
|
|
3
|
+
*
|
|
4
|
+
* 5 read tools (public HL data, no auth):
|
|
5
|
+
* hl_clearinghouse_state, hl_user_fills, hl_meta, hl_all_mids, hl_portfolio
|
|
6
|
+
*
|
|
7
|
+
* 4 write tools (require API wallet; safety rails enforced here):
|
|
8
|
+
* hl_open_position, hl_close_position, hl_modify_position, hl_cancel_orders
|
|
9
|
+
*
|
|
10
|
+
* This module exports a factory function that takes dependencies and returns a
|
|
11
|
+
* list of McpToolDefinition objects. The session orchestrator registers these
|
|
12
|
+
* into the MCP server for each session.
|
|
13
|
+
*
|
|
14
|
+
* It also exports `startMcpServer(config)` — the entry point used by the
|
|
15
|
+
* generated hl-server.mjs wrapper. This is THE live code path for write tools.
|
|
16
|
+
*
|
|
17
|
+
* Safety rails (§8.3) are enforced at the tool boundary before any network call.
|
|
18
|
+
* Claude cannot bypass them through reasoning.
|
|
19
|
+
*/
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
import { encode as msgpackEncode } from '@msgpack/msgpack';
|
|
22
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
23
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
24
|
+
import { HyperliquidClient } from '../../../venues/hyperliquid/client.js';
|
|
25
|
+
import { getUnifiedAccountValue } from '../../../venues/hyperliquid/account-value.js';
|
|
26
|
+
import { checkRateLimit, createRateLimitState, validateOpenPosition, validateClosePosition, validateModifyPosition, DEFAULT_SAFETY_CONFIG, } from './safety-rails.js';
|
|
27
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
28
|
+
function ok(data) {
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function toolErr(code, message) {
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: JSON.stringify({ error: true, code, message }),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// ── Asset index cache ─────────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Module-level cache for coin → asset index lookups (from HL meta endpoint).
|
|
46
|
+
* Populated on first write-tool call per process.
|
|
47
|
+
*/
|
|
48
|
+
const assetIndexCache = new Map();
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a coin name (e.g. "BTC") to its HL asset index via the meta endpoint.
|
|
51
|
+
* Results are cached in-process so subsequent calls are free.
|
|
52
|
+
*
|
|
53
|
+
* Throws with a clear message if the coin is not in the universe.
|
|
54
|
+
*/
|
|
55
|
+
async function resolveAssetIndex(coin, hlClient) {
|
|
56
|
+
if (assetIndexCache.has(coin)) {
|
|
57
|
+
return assetIndexCache.get(coin);
|
|
58
|
+
}
|
|
59
|
+
const meta = await hlClient.meta();
|
|
60
|
+
meta.universe.forEach((asset, idx) => {
|
|
61
|
+
assetIndexCache.set(asset.name, idx);
|
|
62
|
+
assetSzDecimalsCache.set(asset.name, asset.szDecimals);
|
|
63
|
+
});
|
|
64
|
+
if (!assetIndexCache.has(coin)) {
|
|
65
|
+
throw new Error(`Coin "${coin}" not found in HL universe. Available: ${meta.universe.map((u) => u.name).join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
return assetIndexCache.get(coin);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Per-asset size decimals (populated alongside assetIndexCache). HL's perps
|
|
71
|
+
* require px to have ≤ (6 - szDecimals) decimals and sz to have ≤ szDecimals
|
|
72
|
+
* decimals; violating either returns HTTP 422 or a "tick size" business error.
|
|
73
|
+
*/
|
|
74
|
+
const assetSzDecimalsCache = new Map();
|
|
75
|
+
function getSzDecimals(coin) {
|
|
76
|
+
const d = assetSzDecimalsCache.get(coin);
|
|
77
|
+
// Conservative default if cache is empty (caller should have hit resolveAssetIndex first).
|
|
78
|
+
return d ?? 3;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Format a price for HL perps: ≤5 significant figures AND ≤(6-szDecimals) decimals.
|
|
82
|
+
* Strips trailing zeros so the string is canonical (HL's deserializer accepts
|
|
83
|
+
* "123" and "123.45" but rejects "123.00").
|
|
84
|
+
*/
|
|
85
|
+
function formatPxForHl(px, szDecimals) {
|
|
86
|
+
const maxDecimals = Math.max(0, 6 - szDecimals);
|
|
87
|
+
// Round to 5 sig figs (HL rule), then clamp decimal places.
|
|
88
|
+
const sigFigsRounded = parseFloat(px.toPrecision(5));
|
|
89
|
+
const clamped = parseFloat(sigFigsRounded.toFixed(maxDecimals));
|
|
90
|
+
// Number → string; JS omits trailing zeros for non-exponential magnitudes (0.01 to 1e21).
|
|
91
|
+
return clamped.toString();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Format a size for HL: ≤szDecimals decimal places. Keeps trailing zeros stripped
|
|
95
|
+
* (HL's deserializer is lenient about size trailing zeros but canonical is cleaner).
|
|
96
|
+
*/
|
|
97
|
+
function formatSzForHl(size, szDecimals) {
|
|
98
|
+
return parseFloat(size.toFixed(Math.max(0, szDecimals))).toString();
|
|
99
|
+
}
|
|
100
|
+
/** Timeout for fetchOpenOrders and other raw fetch calls (mirrors HyperliquidClient default) */
|
|
101
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
102
|
+
/**
|
|
103
|
+
* Fetch open orders for an address from HL info endpoint.
|
|
104
|
+
* Returns an array of open orders (may be empty).
|
|
105
|
+
* Uses an AbortController-based timeout matching HyperliquidClient.post().
|
|
106
|
+
*/
|
|
107
|
+
async function fetchOpenOrders(baseUrl, address, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
110
|
+
try {
|
|
111
|
+
const resp = await fetch(`${baseUrl}/info`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({ type: 'openOrders', user: address }),
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
});
|
|
117
|
+
if (!resp.ok) {
|
|
118
|
+
const txt = await resp.text().catch(() => '');
|
|
119
|
+
throw new Error(`HL open orders error: HTTP ${resp.status} — ${txt.slice(0, 200)}`);
|
|
120
|
+
}
|
|
121
|
+
return resp.json();
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Sign a Hyperliquid L1 action using the canonical algorithm from hyperliquid-python-sdk.
|
|
129
|
+
*
|
|
130
|
+
* Algorithm (mirrors sign_l1_action in hyperliquid/utils/signing.py):
|
|
131
|
+
*
|
|
132
|
+
* 1. action_hash = keccak256(
|
|
133
|
+
* msgpack(action)
|
|
134
|
+
* || uint64_be(nonce)
|
|
135
|
+
* || vault_flag_byte // 0x01 if vaultAddress present, 0x00 otherwise
|
|
136
|
+
* || vault_address_20_bytes // only if vaultAddress present
|
|
137
|
+
* || (0x01 || uint64_be(expiresAfter)) // only if expiresAfter present
|
|
138
|
+
* )
|
|
139
|
+
*
|
|
140
|
+
* 2. EIP-712 typed-data:
|
|
141
|
+
* domain: { name: "Exchange", version: "1", chainId: 1337, verifyingContract: 0x0...0 }
|
|
142
|
+
* primaryType: "Agent"
|
|
143
|
+
* types: Agent: [{name:"source",type:"string"},{name:"connectionId",type:"bytes32"}]
|
|
144
|
+
* message: { source: isMainnet ? "a" : "b", connectionId: action_hash }
|
|
145
|
+
*
|
|
146
|
+
* 3. Sign with viem signTypedData.
|
|
147
|
+
*
|
|
148
|
+
* 4. Decode 65-byte sig into { r, s, v }.
|
|
149
|
+
*/
|
|
150
|
+
export async function signHlAction(params) {
|
|
151
|
+
const { privateKey, action, nonce, vaultAddress, expiresAfter, isMainnet } = params;
|
|
152
|
+
const { signTypedData } = await import('viem/accounts');
|
|
153
|
+
const { keccak256 } = await import('viem');
|
|
154
|
+
// 1. Build the preimage buffer
|
|
155
|
+
const actionBytes = msgpackEncode(action);
|
|
156
|
+
// uint64 big-endian nonce (8 bytes)
|
|
157
|
+
const nonceBuf = Buffer.alloc(8);
|
|
158
|
+
// Use BigInt to handle the full uint64 range safely
|
|
159
|
+
const nonceBig = BigInt(nonce);
|
|
160
|
+
nonceBuf.writeBigUInt64BE(nonceBig);
|
|
161
|
+
const parts = [actionBytes, nonceBuf];
|
|
162
|
+
if (vaultAddress && vaultAddress !== '0x0000000000000000000000000000000000000000') {
|
|
163
|
+
// vault_flag_byte = 0x01 + 20-byte address
|
|
164
|
+
parts.push(new Uint8Array([0x01]));
|
|
165
|
+
const addrHex = vaultAddress.startsWith('0x') ? vaultAddress.slice(2) : vaultAddress;
|
|
166
|
+
parts.push(Buffer.from(addrHex.padStart(40, '0'), 'hex'));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// vault_flag_byte = 0x00
|
|
170
|
+
parts.push(new Uint8Array([0x00]));
|
|
171
|
+
}
|
|
172
|
+
if (expiresAfter != null) {
|
|
173
|
+
// 0x01 || uint64_be(expiresAfter)
|
|
174
|
+
const expiryBuf = Buffer.alloc(8);
|
|
175
|
+
expiryBuf.writeBigUInt64BE(BigInt(expiresAfter));
|
|
176
|
+
parts.push(new Uint8Array([0x01]));
|
|
177
|
+
parts.push(expiryBuf);
|
|
178
|
+
}
|
|
179
|
+
// Concatenate all parts
|
|
180
|
+
const totalLen = parts.reduce((sum, p) => sum + p.length, 0);
|
|
181
|
+
const preimage = new Uint8Array(totalLen);
|
|
182
|
+
let offset = 0;
|
|
183
|
+
for (const p of parts) {
|
|
184
|
+
preimage.set(p, offset);
|
|
185
|
+
offset += p.length;
|
|
186
|
+
}
|
|
187
|
+
// keccak256 of preimage → connectionId (bytes32)
|
|
188
|
+
const actionHash = keccak256(preimage);
|
|
189
|
+
// 2. Build EIP-712 typed data
|
|
190
|
+
// Note: chainId 1337 is HL's constant for both mainnet and testnet — NOT the EVM chain ID
|
|
191
|
+
const domain = {
|
|
192
|
+
chainId: 1337,
|
|
193
|
+
name: 'Exchange',
|
|
194
|
+
verifyingContract: '0x0000000000000000000000000000000000000000',
|
|
195
|
+
version: '1',
|
|
196
|
+
};
|
|
197
|
+
const types = {
|
|
198
|
+
Agent: [
|
|
199
|
+
{ name: 'source', type: 'string' },
|
|
200
|
+
{ name: 'connectionId', type: 'bytes32' },
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
// source: "a" = mainnet, "b" = testnet
|
|
204
|
+
const source = isMainnet ? 'a' : 'b';
|
|
205
|
+
// 3. Sign
|
|
206
|
+
const sig = await signTypedData({
|
|
207
|
+
privateKey: privateKey,
|
|
208
|
+
domain,
|
|
209
|
+
types,
|
|
210
|
+
primaryType: 'Agent',
|
|
211
|
+
message: {
|
|
212
|
+
source,
|
|
213
|
+
connectionId: actionHash,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
// 4. Decode 65-byte signature: r (32 bytes) + s (32 bytes) + v (1 byte)
|
|
217
|
+
// sig is 0x-prefixed hex, 132 hex chars = 66 bytes including 0x prefix
|
|
218
|
+
const r = `0x${sig.slice(2, 66)}`;
|
|
219
|
+
const s = `0x${sig.slice(66, 130)}`;
|
|
220
|
+
const v = parseInt(sig.slice(130, 132), 16);
|
|
221
|
+
return { r, s, v };
|
|
222
|
+
}
|
|
223
|
+
// ── HL exchange write helper ───────────────────────────────────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* POST to /exchange endpoint using canonical Hyperliquid L1-action signing.
|
|
226
|
+
*
|
|
227
|
+
* Signing follows hyperliquid-python-sdk's sign_l1_action:
|
|
228
|
+
* connectionId = keccak256(msgpack(action) || nonce_u64_be || vault_flag || [vault_addr] || [expiry_flag || expiry_u64_be])
|
|
229
|
+
* EIP-712 Agent{source, connectionId} with domain chainId=1337 (HL constant).
|
|
230
|
+
*
|
|
231
|
+
* @param isMainnet - true for mainnet (source "a"), false for testnet (source "b")
|
|
232
|
+
*/
|
|
233
|
+
async function hlExchangePost(baseUrl, privateKey, action, vaultAddress, isMainnet = false) {
|
|
234
|
+
const nonce = Date.now();
|
|
235
|
+
const { r, s, v } = await signHlAction({
|
|
236
|
+
privateKey,
|
|
237
|
+
action,
|
|
238
|
+
nonce,
|
|
239
|
+
vaultAddress,
|
|
240
|
+
expiresAfter: null,
|
|
241
|
+
isMainnet,
|
|
242
|
+
});
|
|
243
|
+
const body = {
|
|
244
|
+
action,
|
|
245
|
+
nonce,
|
|
246
|
+
signature: { r, s, v },
|
|
247
|
+
};
|
|
248
|
+
if (vaultAddress) {
|
|
249
|
+
body['vaultAddress'] = vaultAddress;
|
|
250
|
+
}
|
|
251
|
+
const resp = await fetch(`${baseUrl}/exchange`, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: { 'Content-Type': 'application/json' },
|
|
254
|
+
body: JSON.stringify(body),
|
|
255
|
+
});
|
|
256
|
+
if (!resp.ok) {
|
|
257
|
+
const txt = await resp.text().catch(() => '');
|
|
258
|
+
throw new Error(`HL exchange error: HTTP ${resp.status} — ${txt.slice(0, 200)}`);
|
|
259
|
+
}
|
|
260
|
+
return resp.json();
|
|
261
|
+
}
|
|
262
|
+
// ── Status inspection helper ───────────────────────────────────────────────────
|
|
263
|
+
/**
|
|
264
|
+
* Inspect response.response?.data?.statuses from an HL exchange response.
|
|
265
|
+
* Throws if any status is an error object.
|
|
266
|
+
* Success statuses: { filled: {...} }, { resting: {...} }, { cancelled: {...} }
|
|
267
|
+
*/
|
|
268
|
+
function checkResponseStatuses(response) {
|
|
269
|
+
const resp = response;
|
|
270
|
+
if (!resp)
|
|
271
|
+
return;
|
|
272
|
+
const inner = resp['response'];
|
|
273
|
+
if (!inner)
|
|
274
|
+
return;
|
|
275
|
+
const data = inner['data'];
|
|
276
|
+
if (!data)
|
|
277
|
+
return;
|
|
278
|
+
const statuses = data['statuses'];
|
|
279
|
+
if (!Array.isArray(statuses))
|
|
280
|
+
return;
|
|
281
|
+
const errors = [];
|
|
282
|
+
for (const status of statuses) {
|
|
283
|
+
const s = status;
|
|
284
|
+
if (s && typeof s['error'] === 'string') {
|
|
285
|
+
errors.push(s['error']);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (errors.length > 0) {
|
|
289
|
+
throw new Error(`HL order error(s): ${errors.join('; ')}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ── Tool factory ───────────────────────────────────────────────────────────────
|
|
293
|
+
/**
|
|
294
|
+
* Build tool handlers for registration with a McpServer.
|
|
295
|
+
*
|
|
296
|
+
* Returns an array of { name, description, schema, handler } objects.
|
|
297
|
+
* The session orchestrator calls server.tool(name, description, schema, handler)
|
|
298
|
+
* for each.
|
|
299
|
+
*/
|
|
300
|
+
export function buildHlTools(deps) {
|
|
301
|
+
const config = deps.safetyConfig ?? DEFAULT_SAFETY_CONFIG;
|
|
302
|
+
const masterAddr = deps.masterAddress;
|
|
303
|
+
// Detect mainnet vs testnet from the base URL
|
|
304
|
+
const isMainnet = deps.hlBaseUrl.includes('hyperliquid.xyz') && !deps.hlBaseUrl.includes('testnet');
|
|
305
|
+
// ── Read tools ─────────────────────────────────────────────────────────────
|
|
306
|
+
const clearinghouseStateTool = {
|
|
307
|
+
name: 'hl_clearinghouse_state',
|
|
308
|
+
description: 'Get the current clearinghouse (margin) state for an HL account. Returns account value, positions, margin summary.',
|
|
309
|
+
schema: z.object({
|
|
310
|
+
address: z.string().optional().describe('HL account address. Defaults to the master account.'),
|
|
311
|
+
}),
|
|
312
|
+
handler: async ({ address }) => {
|
|
313
|
+
try {
|
|
314
|
+
const result = await deps.hlClient.clearinghouseState(address ?? masterAddr);
|
|
315
|
+
return ok(result);
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
return toolErr('HL_API_ERROR', e instanceof Error ? e.message : String(e));
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
const userFillsTool = {
|
|
323
|
+
name: 'hl_user_fills',
|
|
324
|
+
description: 'Get recent fills for an HL account, in descending time order.',
|
|
325
|
+
schema: z.object({
|
|
326
|
+
address: z.string().optional().describe('HL account address. Defaults to the master account.'),
|
|
327
|
+
startTime: z.number().optional().describe('Filter fills after this epoch ms timestamp.'),
|
|
328
|
+
endTime: z.number().optional().describe('Filter fills before this epoch ms timestamp.'),
|
|
329
|
+
limit: z.number().optional().describe('Maximum number of fills to return.'),
|
|
330
|
+
}),
|
|
331
|
+
handler: async ({ address, startTime, endTime, limit }) => {
|
|
332
|
+
try {
|
|
333
|
+
let fills;
|
|
334
|
+
if (startTime !== undefined) {
|
|
335
|
+
const result = await deps.hlClient.userFillsByTime(address ?? masterAddr, startTime, endTime);
|
|
336
|
+
fills = result.fills;
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
fills = await deps.hlClient.userFills(address ?? masterAddr);
|
|
340
|
+
}
|
|
341
|
+
if (limit !== undefined) {
|
|
342
|
+
fills = fills.slice(0, limit);
|
|
343
|
+
}
|
|
344
|
+
return ok(fills);
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
return toolErr('HL_API_ERROR', e instanceof Error ? e.message : String(e));
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
const metaTool = {
|
|
352
|
+
name: 'hl_meta',
|
|
353
|
+
description: 'Get metadata for all HL perpetual markets (names, max leverage, size decimals).',
|
|
354
|
+
schema: z.object({}),
|
|
355
|
+
handler: async () => {
|
|
356
|
+
try {
|
|
357
|
+
const result = await deps.hlClient.meta();
|
|
358
|
+
return ok(result);
|
|
359
|
+
}
|
|
360
|
+
catch (e) {
|
|
361
|
+
return toolErr('HL_API_ERROR', e instanceof Error ? e.message : String(e));
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
const allMidsTool = {
|
|
366
|
+
name: 'hl_all_mids',
|
|
367
|
+
description: 'Get current mid prices for all HL assets.',
|
|
368
|
+
schema: z.object({}),
|
|
369
|
+
handler: async () => {
|
|
370
|
+
try {
|
|
371
|
+
const result = await deps.hlClient.allMids();
|
|
372
|
+
return ok(result);
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
return toolErr('HL_API_ERROR', e instanceof Error ? e.message : String(e));
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
const portfolioTool = {
|
|
380
|
+
name: 'hl_portfolio',
|
|
381
|
+
description: 'Get historical portfolio data (account value history, PnL history) for an HL account.',
|
|
382
|
+
schema: z.object({
|
|
383
|
+
address: z.string().optional().describe('HL account address. Defaults to the master account.'),
|
|
384
|
+
}),
|
|
385
|
+
handler: async ({ address }) => {
|
|
386
|
+
try {
|
|
387
|
+
const result = await deps.hlClient.portfolio(address ?? masterAddr);
|
|
388
|
+
return ok(result);
|
|
389
|
+
}
|
|
390
|
+
catch (e) {
|
|
391
|
+
return toolErr('HL_API_ERROR', e instanceof Error ? e.message : String(e));
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
const accountUnifiedTool = {
|
|
396
|
+
name: 'hl_account_unified',
|
|
397
|
+
description: 'Preferred single-call account read. Returns unified equity (perps + spot USDC), ' +
|
|
398
|
+
'perps-only account value, spot USDC, withdrawable, open positions, and open orders. ' +
|
|
399
|
+
'Use this instead of hl_clearinghouse_state for decisions — the latter omits spot USDC ' +
|
|
400
|
+
'and will mislead you when the master has been spot-funded (perps accountValue reads 0 ' +
|
|
401
|
+
'but cross-margining still lets you trade).',
|
|
402
|
+
schema: z.object({}),
|
|
403
|
+
handler: async () => {
|
|
404
|
+
try {
|
|
405
|
+
const [unified, openOrders] = await Promise.all([
|
|
406
|
+
getUnifiedAccountValue(deps.hlClient, masterAddr),
|
|
407
|
+
fetchOpenOrders(deps.hlBaseUrl, masterAddr).catch(() => []),
|
|
408
|
+
]);
|
|
409
|
+
// HL's assetPositions is typed as unknown[] upstream — each entry is
|
|
410
|
+
// {position: {coin, szi, entryPx, unrealizedPnl, leverage: {...}, liquidationPx, marginUsed}}.
|
|
411
|
+
// We pass the raw structure through so Claude sees the same shape as the
|
|
412
|
+
// info endpoint returns; we also surface the summary fields separately.
|
|
413
|
+
return ok({
|
|
414
|
+
unifiedAccountValue: unified.accountValue,
|
|
415
|
+
perpsAccountValue: unified.perpsAccountValue,
|
|
416
|
+
spotUsdc: unified.spotUsdc,
|
|
417
|
+
withdrawable: unified.clearinghouseState.withdrawable,
|
|
418
|
+
positions: unified.clearinghouseState.assetPositions ?? [],
|
|
419
|
+
openOrders,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
return toolErr('HL_API_ERROR', e instanceof Error ? e.message : String(e));
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
// ── Write tools ────────────────────────────────────────────────────────────
|
|
428
|
+
const openPositionTool = {
|
|
429
|
+
name: 'hl_open_position',
|
|
430
|
+
description: 'Open a new perpetual position on HL. Safety rails enforced: max 25% of account per position, max 10x leverage, max 50 bps slippage.',
|
|
431
|
+
schema: z.object({
|
|
432
|
+
coin: z.string().describe('Asset name, e.g. "BTC"'),
|
|
433
|
+
side: z.enum(['long', 'short']).describe('Position direction'),
|
|
434
|
+
size: z.number().positive().describe('Size in base asset units'),
|
|
435
|
+
leverage: z.number().positive().describe('Desired leverage (1-10)'),
|
|
436
|
+
slippageBps: z.number().int().min(1).max(100).describe('Max slippage in basis points (1-50 enforced)'),
|
|
437
|
+
tp: z.number().positive().optional().describe('Take-profit price in USD. REQUIRED unless bypassRiskRails=true. For a long: tp > entry; for a short: tp < entry.'),
|
|
438
|
+
sl: z.number().positive().optional().describe('Stop-loss price in USD. REQUIRED unless bypassRiskRails=true. For a long: sl < entry; for a short: sl > entry.'),
|
|
439
|
+
bypassRiskRails: z.boolean().optional().describe('Explicitly open without TP/SL. Use only for very short-lived scalps you intend to close this turn. Logged as a deliberate override.'),
|
|
440
|
+
}),
|
|
441
|
+
handler: async ({ coin, side, size, leverage, slippageBps, tp, sl, bypassRiskRails }) => {
|
|
442
|
+
// Rate limit check
|
|
443
|
+
const rlResult = checkRateLimit(deps.rateLimitState, config);
|
|
444
|
+
if (!rlResult.ok)
|
|
445
|
+
return toolErr(rlResult.code, rlResult.message);
|
|
446
|
+
// Re-fetch account value immediately before the notional check to tighten
|
|
447
|
+
// the TOCTOU window. Size against unified equity (perps margin + spot USDC)
|
|
448
|
+
// so spot-funded accounts aren't gated to zero notional.
|
|
449
|
+
let accountValue;
|
|
450
|
+
let midPrice;
|
|
451
|
+
try {
|
|
452
|
+
const [unified, mids] = await Promise.all([
|
|
453
|
+
getUnifiedAccountValue(deps.hlClient, masterAddr),
|
|
454
|
+
deps.hlClient.allMids(),
|
|
455
|
+
]);
|
|
456
|
+
accountValue = parseFloat(unified.accountValue);
|
|
457
|
+
const mid = mids[coin];
|
|
458
|
+
if (!mid) {
|
|
459
|
+
return toolErr('UNKNOWN_COIN', `No mid price for coin "${coin}"`);
|
|
460
|
+
}
|
|
461
|
+
midPrice = parseFloat(mid);
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
return toolErr('HL_API_ERROR', `Failed to fetch market data: ${e instanceof Error ? e.message : String(e)}`);
|
|
465
|
+
}
|
|
466
|
+
// Safety rails — percentage-based cap
|
|
467
|
+
const validation = validateOpenPosition({ coin, side, size, leverage, midPrice, accountValue, slippageBps, tp, sl }, config);
|
|
468
|
+
if (!validation.ok)
|
|
469
|
+
return toolErr(validation.code, validation.message);
|
|
470
|
+
// Hard absolute notional cap (finding #15): effective cap = min(pctCap, maxNotionalUsd)
|
|
471
|
+
const notionalUsd = size * midPrice;
|
|
472
|
+
const pctCap = config.maxPositionFraction * accountValue;
|
|
473
|
+
const effectiveCap = config.maxNotionalUsd != null
|
|
474
|
+
? Math.min(pctCap, config.maxNotionalUsd)
|
|
475
|
+
: pctCap;
|
|
476
|
+
if (notionalUsd > effectiveCap) {
|
|
477
|
+
return toolErr('NOTIONAL_EXCEEDED', `Notional $${notionalUsd.toFixed(2)} exceeds hard cap $${effectiveCap.toFixed(2)} (min of ${(config.maxPositionFraction * 100).toFixed(0)}% of $${accountValue.toFixed(2)} = $${pctCap.toFixed(2)}${config.maxNotionalUsd != null ? ` and maxNotionalUsd=$${config.maxNotionalUsd}` : ''})`);
|
|
478
|
+
}
|
|
479
|
+
// Resolve asset index from coin name
|
|
480
|
+
let assetIdx;
|
|
481
|
+
try {
|
|
482
|
+
assetIdx = await resolveAssetIndex(coin, deps.hlClient);
|
|
483
|
+
}
|
|
484
|
+
catch (e) {
|
|
485
|
+
return toolErr('UNKNOWN_COIN', e instanceof Error ? e.message : String(e));
|
|
486
|
+
}
|
|
487
|
+
// Build HL order action
|
|
488
|
+
const isBuy = side === 'long';
|
|
489
|
+
const slippage = slippageBps / 10000;
|
|
490
|
+
const limitPx = isBuy
|
|
491
|
+
? midPrice * (1 + slippage)
|
|
492
|
+
: midPrice * (1 - slippage);
|
|
493
|
+
const szDec = getSzDecimals(coin);
|
|
494
|
+
// Tool-level risk rail: both TP and SL must be set on every open, or the
|
|
495
|
+
// caller must explicitly acknowledge they're opening bare via
|
|
496
|
+
// bypassRiskRails=true. Prevents silent open-without-stop scenarios where
|
|
497
|
+
// a Claude session could skip mentioning sl and leave tail risk unbounded
|
|
498
|
+
// until its next ~30-min wakeup.
|
|
499
|
+
const hasTp = tp !== undefined;
|
|
500
|
+
const hasSl = sl !== undefined;
|
|
501
|
+
if ((!hasTp || !hasSl) && !bypassRiskRails) {
|
|
502
|
+
return toolErr('TPSL_REQUIRED', `hl_open_position requires both tp (take-profit) and sl (stop-loss). ` +
|
|
503
|
+
`Got tp=${tp}, sl=${sl}. For a ${side}: sl must be ${isBuy ? 'below' : 'above'} the mid ($${midPrice}) and tp must be ${isBuy ? 'above' : 'below'} it. ` +
|
|
504
|
+
`If you intentionally want to open bare (e.g. a scalp you will close this same turn), pass bypassRiskRails=true.`);
|
|
505
|
+
}
|
|
506
|
+
// Sanity: tp/sl on the correct sides of mid.
|
|
507
|
+
if (hasTp) {
|
|
508
|
+
if (isBuy && tp <= midPrice) {
|
|
509
|
+
return toolErr('TP_INVALID', `tp ${tp} must be > mid ${midPrice} for a long`);
|
|
510
|
+
}
|
|
511
|
+
if (!isBuy && tp >= midPrice) {
|
|
512
|
+
return toolErr('TP_INVALID', `tp ${tp} must be < mid ${midPrice} for a short`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (hasSl) {
|
|
516
|
+
if (isBuy && sl >= midPrice) {
|
|
517
|
+
return toolErr('SL_INVALID', `sl ${sl} must be < mid ${midPrice} for a long`);
|
|
518
|
+
}
|
|
519
|
+
if (!isBuy && sl <= midPrice) {
|
|
520
|
+
return toolErr('SL_INVALID', `sl ${sl} must be > mid ${midPrice} for a short`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Parent position-opening order (IOC for slippage control).
|
|
524
|
+
// HL's inline-trigger + positionTpsl grouping rejects with opaque
|
|
525
|
+
// "Trigger order has unexpected type" errors on some testnets, so we
|
|
526
|
+
// submit the parent first and then attach TP/SL as standalone
|
|
527
|
+
// reduce-only trigger orders — simpler wire shape, easier to debug.
|
|
528
|
+
const parentOrder = {
|
|
529
|
+
a: assetIdx,
|
|
530
|
+
b: isBuy,
|
|
531
|
+
p: formatPxForHl(limitPx, szDec),
|
|
532
|
+
s: formatSzForHl(size, szDec),
|
|
533
|
+
r: false,
|
|
534
|
+
t: { limit: { tif: 'Ioc' } },
|
|
535
|
+
};
|
|
536
|
+
const parentAction = {
|
|
537
|
+
type: 'order',
|
|
538
|
+
orders: [parentOrder],
|
|
539
|
+
grouping: 'na',
|
|
540
|
+
};
|
|
541
|
+
const submittedAt = Date.now();
|
|
542
|
+
let parentResponse;
|
|
543
|
+
try {
|
|
544
|
+
parentResponse = await hlExchangePost(deps.hlBaseUrl, deps.apiWalletPrivateKey, parentAction, undefined, isMainnet);
|
|
545
|
+
checkResponseStatuses(parentResponse);
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
return toolErr('HL_EXCHANGE_ERROR', e instanceof Error ? e.message : String(e));
|
|
549
|
+
}
|
|
550
|
+
// If the operator chose to bypass the risk rails, we're done — just the parent.
|
|
551
|
+
if (!hasTp && !hasSl) {
|
|
552
|
+
const record = { tool: 'hl_open_position', submittedAt, params: { coin, side, size, leverage, slippageBps, tp, sl, bypassRiskRails }, response: parentResponse };
|
|
553
|
+
deps.onWriteOp?.(record);
|
|
554
|
+
return ok({ submitted: true, response: parentResponse, submittedAt });
|
|
555
|
+
}
|
|
556
|
+
// Place TP/SL as standalone reduce-only trigger orders. HL binds them to
|
|
557
|
+
// the resulting position because r=true + same asset + opposite side.
|
|
558
|
+
const triggerOrders = [];
|
|
559
|
+
if (hasTp) {
|
|
560
|
+
triggerOrders.push({
|
|
561
|
+
a: assetIdx,
|
|
562
|
+
b: !isBuy,
|
|
563
|
+
p: formatPxForHl(tp, szDec),
|
|
564
|
+
s: formatSzForHl(size, szDec),
|
|
565
|
+
r: true,
|
|
566
|
+
t: { trigger: { isMarket: true, triggerPx: formatPxForHl(tp, szDec), tpsl: 'tp' } },
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (hasSl) {
|
|
570
|
+
triggerOrders.push({
|
|
571
|
+
a: assetIdx,
|
|
572
|
+
b: !isBuy,
|
|
573
|
+
p: formatPxForHl(sl, szDec),
|
|
574
|
+
s: formatSzForHl(size, szDec),
|
|
575
|
+
r: true,
|
|
576
|
+
t: { trigger: { isMarket: true, triggerPx: formatPxForHl(sl, szDec), tpsl: 'sl' } },
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
let triggerResponse = null;
|
|
580
|
+
let triggerError = null;
|
|
581
|
+
if (triggerOrders.length > 0) {
|
|
582
|
+
const triggerAction = {
|
|
583
|
+
type: 'order',
|
|
584
|
+
orders: triggerOrders,
|
|
585
|
+
grouping: 'na',
|
|
586
|
+
};
|
|
587
|
+
try {
|
|
588
|
+
triggerResponse = await hlExchangePost(deps.hlBaseUrl, deps.apiWalletPrivateKey, triggerAction, undefined, isMainnet);
|
|
589
|
+
checkResponseStatuses(triggerResponse);
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
triggerError = e instanceof Error ? e.message : String(e);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const record = {
|
|
596
|
+
tool: 'hl_open_position',
|
|
597
|
+
submittedAt,
|
|
598
|
+
params: { coin, side, size, leverage, slippageBps, tp, sl, bypassRiskRails },
|
|
599
|
+
response: { parent: parentResponse, triggers: triggerResponse, triggerError },
|
|
600
|
+
};
|
|
601
|
+
deps.onWriteOp?.(record);
|
|
602
|
+
if (triggerError) {
|
|
603
|
+
// Parent filled but triggers failed — surface the failure so the caller
|
|
604
|
+
// can decide to manually close or re-submit triggers. The position is
|
|
605
|
+
// LIVE and UNPROTECTED at this point.
|
|
606
|
+
return toolErr('TRIGGERS_FAILED', `Parent order submitted successfully but TP/SL trigger orders failed: ${triggerError}. Position is open WITHOUT protection — close immediately via hl_close_position or retry triggers.`);
|
|
607
|
+
}
|
|
608
|
+
return ok({ submitted: true, parent: parentResponse, triggers: triggerResponse, submittedAt });
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
const closePositionTool = {
|
|
612
|
+
name: 'hl_close_position',
|
|
613
|
+
description: 'Close an existing perpetual position on HL.',
|
|
614
|
+
schema: z.object({
|
|
615
|
+
coin: z.string().describe('Asset name, e.g. "BTC"'),
|
|
616
|
+
sizeOrAll: z.union([z.number().positive(), z.literal('all')]).describe('Size to close, or "all" to close entire position'),
|
|
617
|
+
}),
|
|
618
|
+
handler: async ({ coin, sizeOrAll }) => {
|
|
619
|
+
const rlResult = checkRateLimit(deps.rateLimitState, config);
|
|
620
|
+
if (!rlResult.ok)
|
|
621
|
+
return toolErr(rlResult.code, rlResult.message);
|
|
622
|
+
const validation = validateClosePosition({ coin, sizeOrAll });
|
|
623
|
+
if (!validation.ok)
|
|
624
|
+
return toolErr(validation.code, validation.message);
|
|
625
|
+
// Build close action — reduce-only order at market
|
|
626
|
+
let mids;
|
|
627
|
+
try {
|
|
628
|
+
mids = await deps.hlClient.allMids();
|
|
629
|
+
}
|
|
630
|
+
catch (e) {
|
|
631
|
+
return toolErr('HL_API_ERROR', `Failed to fetch mids: ${e instanceof Error ? e.message : String(e)}`);
|
|
632
|
+
}
|
|
633
|
+
const mid = mids[coin];
|
|
634
|
+
if (!mid) {
|
|
635
|
+
return toolErr('UNKNOWN_COIN', `No mid price for coin "${coin}"`);
|
|
636
|
+
}
|
|
637
|
+
const midPrice = parseFloat(mid);
|
|
638
|
+
const slippage = config.maxSlippageBps / 10000;
|
|
639
|
+
// Resolve asset index from coin name
|
|
640
|
+
let assetIdx;
|
|
641
|
+
try {
|
|
642
|
+
assetIdx = await resolveAssetIndex(coin, deps.hlClient);
|
|
643
|
+
}
|
|
644
|
+
catch (e) {
|
|
645
|
+
return toolErr('UNKNOWN_COIN', e instanceof Error ? e.message : String(e));
|
|
646
|
+
}
|
|
647
|
+
// For close, determine direction from existing position
|
|
648
|
+
// We use reduce-only flag and set price conservatively
|
|
649
|
+
const closeSz = sizeOrAll === 'all' ? '0' : formatSzForHl(sizeOrAll, getSzDecimals(coin));
|
|
650
|
+
const action = {
|
|
651
|
+
type: 'order',
|
|
652
|
+
orders: [
|
|
653
|
+
{
|
|
654
|
+
a: assetIdx,
|
|
655
|
+
b: false, // sell to close long (conservative; works for both if r=true)
|
|
656
|
+
p: formatPxForHl(midPrice * (1 - slippage), getSzDecimals(coin)),
|
|
657
|
+
s: closeSz,
|
|
658
|
+
r: true, // reduce-only
|
|
659
|
+
t: { limit: { tif: 'Ioc' } },
|
|
660
|
+
// cloid omitted — see hl_open_position for rationale.
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
grouping: 'na',
|
|
664
|
+
};
|
|
665
|
+
const submittedAt = Date.now();
|
|
666
|
+
try {
|
|
667
|
+
const response = await hlExchangePost(deps.hlBaseUrl, deps.apiWalletPrivateKey, action, undefined, isMainnet);
|
|
668
|
+
// Inspect per-order statuses — throw if any order reported an error
|
|
669
|
+
checkResponseStatuses(response);
|
|
670
|
+
const record = { tool: 'hl_close_position', submittedAt, params: { coin, sizeOrAll }, response };
|
|
671
|
+
deps.onWriteOp?.(record);
|
|
672
|
+
return ok({ submitted: true, response, submittedAt });
|
|
673
|
+
}
|
|
674
|
+
catch (e) {
|
|
675
|
+
return toolErr('HL_EXCHANGE_ERROR', e instanceof Error ? e.message : String(e));
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
const modifyPositionTool = {
|
|
680
|
+
name: 'hl_modify_position',
|
|
681
|
+
description: 'Modify an existing position on HL: change leverage, or update TP/SL.',
|
|
682
|
+
schema: z.object({
|
|
683
|
+
coin: z.string().describe('Asset name, e.g. "BTC"'),
|
|
684
|
+
leverage: z.number().optional().describe('New leverage (1-10)'),
|
|
685
|
+
tp: z.number().optional().describe('New take-profit price in USD'),
|
|
686
|
+
sl: z.number().optional().describe('New stop-loss price in USD'),
|
|
687
|
+
}),
|
|
688
|
+
handler: async ({ coin, leverage, tp, sl }) => {
|
|
689
|
+
const rlResult = checkRateLimit(deps.rateLimitState, config);
|
|
690
|
+
if (!rlResult.ok)
|
|
691
|
+
return toolErr(rlResult.code, rlResult.message);
|
|
692
|
+
const validation = validateModifyPosition({ coin, leverage, tp, sl }, config);
|
|
693
|
+
if (!validation.ok)
|
|
694
|
+
return toolErr(validation.code, validation.message);
|
|
695
|
+
if (leverage === undefined && tp === undefined && sl === undefined) {
|
|
696
|
+
return toolErr('NO_OP', 'No modification parameters provided (leverage, tp, or sl required)');
|
|
697
|
+
}
|
|
698
|
+
// TP/SL modification: the correct HL action shape for updating TP/SL on an
|
|
699
|
+
// existing position is not yet confirmed. The 'updateLeverage' type used in v0
|
|
700
|
+
// was incorrect. Returning a structured error until the correct action shape
|
|
701
|
+
// is verified and implemented.
|
|
702
|
+
// TODO: implement TP/SL update once correct HL action shape is confirmed.
|
|
703
|
+
if (tp !== undefined || sl !== undefined) {
|
|
704
|
+
return toolErr('TPSL_NOT_IMPLEMENTED', 'TP/SL modification is not yet implemented in v0 — the correct HL exchange action shape needs verification. Use hl_open_position with tp/sl params for new positions.');
|
|
705
|
+
}
|
|
706
|
+
// Resolve asset index from coin name
|
|
707
|
+
let assetIdx;
|
|
708
|
+
try {
|
|
709
|
+
assetIdx = await resolveAssetIndex(coin, deps.hlClient);
|
|
710
|
+
}
|
|
711
|
+
catch (e) {
|
|
712
|
+
return toolErr('UNKNOWN_COIN', e instanceof Error ? e.message : String(e));
|
|
713
|
+
}
|
|
714
|
+
const submittedAt = Date.now();
|
|
715
|
+
const ops = [];
|
|
716
|
+
if (leverage !== undefined) {
|
|
717
|
+
ops.push({
|
|
718
|
+
label: 'leverage',
|
|
719
|
+
action: {
|
|
720
|
+
type: 'leverage',
|
|
721
|
+
asset: assetIdx,
|
|
722
|
+
isCross: true,
|
|
723
|
+
leverage,
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
const results = [];
|
|
729
|
+
for (const op of ops) {
|
|
730
|
+
const response = await hlExchangePost(deps.hlBaseUrl, deps.apiWalletPrivateKey, op.action, undefined, isMainnet);
|
|
731
|
+
results.push({ label: op.label, response });
|
|
732
|
+
}
|
|
733
|
+
const record = { tool: 'hl_modify_position', submittedAt, params: { coin, leverage, tp, sl }, response: results };
|
|
734
|
+
deps.onWriteOp?.(record);
|
|
735
|
+
return ok({ submitted: true, results, submittedAt });
|
|
736
|
+
}
|
|
737
|
+
catch (e) {
|
|
738
|
+
return toolErr('HL_EXCHANGE_ERROR', e instanceof Error ? e.message : String(e));
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
const cancelOrdersTool = {
|
|
743
|
+
name: 'hl_cancel_orders',
|
|
744
|
+
description: 'Cancel open orders on HL. Optionally filter by coin.',
|
|
745
|
+
schema: z.object({
|
|
746
|
+
coin: z.string().optional().describe('Cancel orders only for this asset. If omitted, cancels all open orders.'),
|
|
747
|
+
}),
|
|
748
|
+
handler: async ({ coin }) => {
|
|
749
|
+
const rlResult = checkRateLimit(deps.rateLimitState, config);
|
|
750
|
+
if (!rlResult.ok)
|
|
751
|
+
return toolErr(rlResult.code, rlResult.message);
|
|
752
|
+
// Fetch open orders to get real order IDs
|
|
753
|
+
let openOrders;
|
|
754
|
+
try {
|
|
755
|
+
openOrders = await fetchOpenOrders(deps.hlBaseUrl, masterAddr);
|
|
756
|
+
}
|
|
757
|
+
catch (e) {
|
|
758
|
+
return toolErr('HL_API_ERROR', `Failed to fetch open orders: ${e instanceof Error ? e.message : String(e)}`);
|
|
759
|
+
}
|
|
760
|
+
// Filter by coin if specified
|
|
761
|
+
const ordersToCancel = coin
|
|
762
|
+
? openOrders.filter((o) => o.coin === coin)
|
|
763
|
+
: openOrders;
|
|
764
|
+
if (ordersToCancel.length === 0) {
|
|
765
|
+
return ok({ submitted: false, cancelled: 0, message: coin ? `No open orders for ${coin}` : 'No open orders to cancel' });
|
|
766
|
+
}
|
|
767
|
+
// Resolve asset indices for all unique coins in the orders to cancel
|
|
768
|
+
const coinAssetMap = new Map();
|
|
769
|
+
try {
|
|
770
|
+
const meta = await deps.hlClient.meta();
|
|
771
|
+
meta.universe.forEach((asset, idx) => {
|
|
772
|
+
assetIndexCache.set(asset.name, idx);
|
|
773
|
+
coinAssetMap.set(asset.name, idx);
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
catch (e) {
|
|
777
|
+
return toolErr('HL_API_ERROR', `Failed to fetch meta for asset index: ${e instanceof Error ? e.message : String(e)}`);
|
|
778
|
+
}
|
|
779
|
+
// Build cancel entries: { a: assetIndex, o: orderId }
|
|
780
|
+
const cancels = [];
|
|
781
|
+
for (const order of ordersToCancel) {
|
|
782
|
+
const assetIdx = coinAssetMap.get(order.coin);
|
|
783
|
+
if (assetIdx === undefined) {
|
|
784
|
+
return toolErr('UNKNOWN_COIN', `No asset index for coin "${order.coin}" in open order`);
|
|
785
|
+
}
|
|
786
|
+
cancels.push({ a: assetIdx, o: order.oid });
|
|
787
|
+
}
|
|
788
|
+
const action = {
|
|
789
|
+
type: 'cancel',
|
|
790
|
+
cancels,
|
|
791
|
+
};
|
|
792
|
+
const submittedAt = Date.now();
|
|
793
|
+
try {
|
|
794
|
+
const response = await hlExchangePost(deps.hlBaseUrl, deps.apiWalletPrivateKey, action, undefined, isMainnet);
|
|
795
|
+
// Inspect per-order statuses — throw if any cancel reported an error
|
|
796
|
+
checkResponseStatuses(response);
|
|
797
|
+
const record = { tool: 'hl_cancel_orders', submittedAt, params: { coin, cancelCount: cancels.length }, response };
|
|
798
|
+
deps.onWriteOp?.(record);
|
|
799
|
+
return ok({ submitted: true, cancelled: cancels.length, response, submittedAt });
|
|
800
|
+
}
|
|
801
|
+
catch (e) {
|
|
802
|
+
return toolErr('HL_EXCHANGE_ERROR', e instanceof Error ? e.message : String(e));
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
};
|
|
806
|
+
return [
|
|
807
|
+
accountUnifiedTool,
|
|
808
|
+
clearinghouseStateTool,
|
|
809
|
+
userFillsTool,
|
|
810
|
+
metaTool,
|
|
811
|
+
allMidsTool,
|
|
812
|
+
portfolioTool,
|
|
813
|
+
openPositionTool,
|
|
814
|
+
closePositionTool,
|
|
815
|
+
modifyPositionTool,
|
|
816
|
+
cancelOrdersTool,
|
|
817
|
+
];
|
|
818
|
+
}
|
|
819
|
+
// ── MCP server entry point ─────────────────────────────────────────────────────
|
|
820
|
+
/**
|
|
821
|
+
* Start the HL MCP server on stdio using the provided config.
|
|
822
|
+
*
|
|
823
|
+
* This is THE live code path for write tools in production. The generated
|
|
824
|
+
* hl-server.mjs wrapper imports and calls this function — no stub signing,
|
|
825
|
+
* no duplication, no split-brain.
|
|
826
|
+
*
|
|
827
|
+
* Stays alive until stdin closes (child process lifecycle).
|
|
828
|
+
*/
|
|
829
|
+
export async function startMcpServer(config) {
|
|
830
|
+
const hlClient = new HyperliquidClient(config.hlBaseUrl);
|
|
831
|
+
const rateLimitState = createRateLimitState();
|
|
832
|
+
const safetyConfig = config.safetyConfig
|
|
833
|
+
? { ...DEFAULT_SAFETY_CONFIG, ...config.safetyConfig }
|
|
834
|
+
: DEFAULT_SAFETY_CONFIG;
|
|
835
|
+
const deps = {
|
|
836
|
+
hlClient,
|
|
837
|
+
hlBaseUrl: config.hlBaseUrl,
|
|
838
|
+
apiWalletPrivateKey: config.apiWalletPrivateKey,
|
|
839
|
+
apiWalletAddress: config.apiWalletAddress,
|
|
840
|
+
masterAddress: config.masterAddress,
|
|
841
|
+
safetyConfig,
|
|
842
|
+
rateLimitState,
|
|
843
|
+
};
|
|
844
|
+
const tools = buildHlTools(deps);
|
|
845
|
+
const server = new McpServer({ name: 'jinn-hl', version: '1.0.0' });
|
|
846
|
+
// Use 'any' cast for handler registration to avoid fighting MCP's CallToolResult
|
|
847
|
+
// index-signature requirement. McpToolResult is structurally compatible at runtime.
|
|
848
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
849
|
+
const registerTool = server.tool.bind(server);
|
|
850
|
+
for (const tool of tools) {
|
|
851
|
+
// Extract the Zod shape from the z.object() schema for MCP server registration.
|
|
852
|
+
// All HlToolDefinition schemas are z.object() instances.
|
|
853
|
+
const schema = tool.schema;
|
|
854
|
+
const shape = schema.shape ?? {};
|
|
855
|
+
registerTool(tool.name, tool.description, shape, async (args) => tool.handler(args));
|
|
856
|
+
}
|
|
857
|
+
const transport = new StdioServerTransport();
|
|
858
|
+
await server.connect(transport);
|
|
859
|
+
// Keep alive until stdin closes
|
|
860
|
+
await new Promise((resolve) => {
|
|
861
|
+
process.stdin.on('close', resolve);
|
|
862
|
+
process.stdin.on('end', resolve);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
//# sourceMappingURL=mcp-tools.js.map
|