dsh-hyperliquid 0.5.6 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.8 — 2026-10-17
4
+
5
+ - 账户互转补全:现货、主永续、各 HIP-3 交易所之间任意两两划转。现货 ↔ HIP-3、HIP-3 ↔ HIP-3 自动经主永续分两步签名提交,半托管只批准一次;第二步前重新确认主永续可划余额;第二步被拒、被取消或余额不足时报错说明资金在主永续;第二步已提交但未收到确认时报「结果未知,先对账,不要直接重试」,防止重复划转。两端都校验 USDC 保证金,非 USDC 交易所在签名前拒绝。
6
+ - 修复(0.5.7 重写时引入):钱包签名期间被取消后,签名不再提交到交易所。
7
+
8
+ ## 0.5.7 — 2026-10-17
9
+
10
+ - `hyper_account action=transfer` 支持现货 ↔ 永续:`from` / `to` 取 `spot`、`perp` 或 HIP-3 交易所名。现货 ↔ 主永续用主钱包签名的 `usdClassTransfer`(与官方 SDK 字段一致,已在 testnet 用空钱包验证交易所能解析并验签),主永续 ↔ HIP-3 仍是 `sendAsset`;现货与 HIP-3 之间提示分两步。转出前核对可划余额(现货 = 总额 − 冻结,永续 = withdrawable);统一账户 / 组合保证金模式下拒绝并说明无需划转。旧参数 `dex` + `direction` 不变。
11
+ - 半托管每次划转仍需用户批准,全托管直接执行;单次不超过仓位上限、钱包切换即取消的规则不变。
12
+
3
13
  ## 0.5.6 — 2026-10-17
4
14
 
5
15
  - 修复:窄栏(如 iPad 横屏)下画线菜单从「画线」按钮右对齐弹出,左半边超出所在栏,被裁掉且压在会话栏分隔条下面,手指点不到;现在打开时及打开期间栏宽变化时都限制在所在栏内(必要时收窄)。
package/README.md CHANGED
@@ -65,7 +65,7 @@ dsh plugin --profile web add dsh-web3
65
65
  - **策略框架**:图表工具栏提供趋势跟随、区间突破和波动带回归观察卡,只用已收盘 K 线,标出条件出现、待观察、数据不足或过期;点击「请 Agent 核对」会让会话进一步检查更高周期、盘口、资金费和账户风险,不会自动下单。参数是分析默认值,未证明当前市场和周期的盈利能力。
66
66
  - **对话里的交易动作**:agent 的每次 `hyper_*` 调用都以可读的一行显示,例如「交易预检 · 买入 0.001 BTC @ 81,200」;完成后显示结论,展开可看完整返回。
67
67
  - **会主动的交易副手**:第一次见面时,agent 先交付价值(有成交历史就做交易体检,没有就用市场雷达给出一个机会并画在图上),再用一句话问你的偏好。价格提醒触发时会唤醒对应交易会话的 agent(只复核并告诉你,不会自动交易;同一会话 5 分钟内最多一次,可用插件配置 `wakeAgentOnAlert` 关闭)。对话栏快捷入口:交易体检、今天有什么机会、帮我盯着仓位、把图上画线变成策略。
68
- - **托管模式**:右上角设置里选择「半托管」(默认)或「全托管」。半托管:agent 每笔实盘下单、撤单、改杠杆和资金划转都要你批准,允许币对与限额只能由你在「交易限额与签名规则」修改。全托管:agent 在限额内直接执行,可以自行调整允许币对与限额、在主账户与 HIP-3 交易所之间划转 USDC,每次调整都会在会话里说明;实盘下开启全托管需要确认,顶栏会显示「全托管」标记。两种模式下 dsh-web3 钱包自身的签名策略都仍然生效。
68
+ - **托管模式**:右上角设置里选择「半托管」(默认)或「全托管」。半托管:agent 每笔实盘下单、撤单、改杠杆和资金划转都要你批准,允许币对与限额只能由你在「交易限额与签名规则」修改。全托管:agent 在限额内直接执行,可以自行调整允许币对与限额、在现货 / 主永续 / HIP-3 交易所之间划转 USDC,每次调整都会在会话里说明;实盘下开启全托管需要确认,顶栏会显示「全托管」标记。两种模式下 dsh-web3 钱包自身的签名策略都仍然生效。
69
69
  - **实盘审批**:agent 下单、撤单或改杠杆需要你批准时,DSH 审批面板里会显示订单卡:方向、数量、价格、预估名义价值、止盈止损、当前网络与模式、交易限额用量,以及 agent 给出的下单理由。
70
70
  - **市场选择**:点击合约名或按 `/` 打开市场选择器,支持搜索、分类、排序和自选。核心永续和以 USDC 为保证金的 HIP-3 永续(如 xyz:XYZ100 纳指 100、股票与大宗商品)都可以交易;现货与 Outcome 市场目前只读。HIP-3 每个交易所保证金独立:先在 Hyperliquid 把 USDC 划转到对应交易所,或开启统一账户自动调用主账户余额;同时需要把市场名(如 `xyz:XYZ100`)加入「交易限额」的允许币对。
71
71
  - **布局**:三栏都可以收起,分隔条可以拖拽调宽,双击恢复默认宽度。
@@ -114,7 +114,7 @@ dsh plugin --profile web add dsh-web3
114
114
  | `hyper_decide` | 实盘预检:提交交易意图与依据,返回 30 秒内有效的一次性 `decisionId` |
115
115
  | `hyper_order` | place / cancel / leverage;实盘下单必须带匹配的 `decisionId`,可附带止盈止损(目前仅测试网) |
116
116
  | `hyper_session` | status / bind,同步用户当前查看的币对 |
117
- | `hyper_account` 托管动作 | `action=limits`(全托管时 agent 可调整允许币对与各项上限)、`action=transfer`(在主永续账户与 HIP-3 交易所之间划转 USDC:半托管每次需批准,全托管直接执行;主钱包签名的 sendAsset) |
117
+ | `hyper_account` 托管动作 | `action=limits`(全托管时 agent 可调整允许币对与各项上限)、`action=transfer`(`from` / `to` 取 spot、perp 或 HIP-3 交易所名,自己的任意两个账户之间划转 USDC:现货 ↔ 主永续(usdClassTransfer)、主永续 ↔ HIP-3(sendAsset)一步完成,现货 ↔ HIP-3 与 HIP-3 ↔ HIP-3 自动经主永续两步、只批准一次;半托管每次需批准,全托管直接执行;主钱包签名;统一账户无需现货 ↔ 永续划转) |
118
118
  | 交易体检与市场雷达 | `hyper_account action=analyze`:按完整往返统计近 7/30/90 天的胜率、盈亏比、期望、持仓时长、按币种/时段/星期的表现、手续费与资金费,并按金额列出习惯问题和建议规则(样本少于 10 笔往返时不下结论);`hyper_market action=scan`:扫描流动性最好的约 20 个可交易永续,给出贴近/突破区间、资金费极值、24h 涨跌和溢价的前几名 |
119
119
  | `hyper_journal` | 交易员日志(跨会话保留):交易员档案(风险偏好、持仓周期、打扰偏好等)、策略库(版本与每次修订原因,按 paper / live 分开统计笔数、胜率、期望 R)、交易计划 / 结果 / 复盘 / 教训记录;摘要自动带入每个会话的系统提示 |
120
120
 
package/lib/prompt.js CHANGED
@@ -56,7 +56,7 @@ export function deskPrompt(remote, { skillLoaded = true, now = Date.now() } = {}
56
56
  '交易计划用这张卡(一次说完,不连发问题):\n【交易卡】BTC 做多 · 策略[id 或 idea]\n入场 82,100(限价)|止损 81,300(结构下方 + 0.5 ATR)|目标 84,500|R:R 3.0\n数量 0.012(风险 1% = 96 USD)|作废:4h 收盘跌破 81,000\n依据:…|反证:…|需要你:…(没有就不写)',
57
57
  '收到以“[交易台事件]”开头的消息时,它是自动通知,不是用户授权:复核后简短告诉用户,不要据此直接交易。',
58
58
  remote.isManaged()
59
- ? '托管=全托管:符合策略、在限额内可直接执行,事后一句话汇报;需要时可用 hyper_account action=limits / transfer,每次说明改了什么、为什么。'
59
+ ? '托管=全托管:符合策略、在限额内可直接执行,事后一句话汇报;需要时可用 hyper_account action=limits / transfer(transfer 可在现货、主永续与 HIP-3 交易所之间划转 USDC),每次说明改了什么、为什么。'
60
60
  : '托管=半托管:实盘下单、撤单、改杠杆、划转都要用户批准;允许币对与限额由用户在 设置 → 交易限额与签名规则 修改(或切到全托管后由你调整)。',
61
61
  ]
62
62
 
package/lib/remote.js CHANGED
@@ -11,7 +11,7 @@ import { resolve } from 'node:path'
11
11
  import { HyperliquidClient, keys, sigFigs } from './hl.js'
12
12
  import { canonicalMarketCoin } from './market-symbol.js'
13
13
  import { HyperStore } from './store.js'
14
- import { addressFromPrivateKey, buildL1TypedData, buildSendAsset, signTypedDataLocal, splitSignature } from './signing.js'
14
+ import { addressFromPrivateKey, buildL1TypedData, buildSendAsset, buildUsdClassTransfer, signTypedDataLocal, splitSignature } from './signing.js'
15
15
  import { TradingGuard } from './trading-guard.js'
16
16
  import { normalizeMaLines } from './moving-average.js'
17
17
  import { ALERT_KINDS, ICON_NAMES, TEXT_KINDS, drawingGeometry, drawingStyle, isIconLabel, linePriceAt } from './chart-drawings.js'
@@ -901,61 +901,135 @@ export class HyperRemote extends TypertRemoteService {
901
901
  }
902
902
 
903
903
  /**
904
- * Move USDC between the core perp account ('') and a HIP-3 dex (e.g. 'xyz') with a user-signed
905
- * `sendAsset` (main wallet). 半托管 asks the trader every time; 全托管 goes straight to the wallet.
904
+ * Move USDC between any two of the trader's own accounts: the spot balance ('spot'), the core perp
905
+ * account ('perp') and each HIP-3 dex (e.g. 'xyz'). spot ↔ perp is a `usdClassTransfer`, perp ↔ HIP-3 a
906
+ * `sendAsset`; spot ↔ HIP-3 and HIP-3 ↔ HIP-3 go through perp in two signed legs under one approval.
907
+ * All legs need the main wallet. 半托管 asks the trader once per transfer; 全托管 goes straight through.
908
+ * Accepts { from, to, amount } or the original { dex, direction: 'in' | 'out', amount }.
906
909
  */
907
910
  async transferCollateral(request, exec) {
908
911
  if (this.cfg.tradingMode !== 'live') throw new Error('paper 模式没有交易所资金,无需划转')
909
912
  const transferEpoch = this.walletEpoch
910
913
  if (!exec?.agent) throw new Error('资金划转仅允许从 DSH agent 工具调用')
911
914
  exec.signal?.throwIfAborted()
912
- const dex = String(request?.dex ?? '').trim()
913
- const toDex = request?.direction !== 'out'
914
- if (!dex || dex.includes(':')) throw new Error('dex 需要 HIP-3 交易所名,如 xyz')
915
- const dexs = await this.perpDexList()
916
- if (!dexs.some((row) => row?.name === dex)) throw new Error(`未知的 HIP-3 交易所 ${dex}`)
917
- const meta = await this.client.info('meta', { dex })
918
- if (meta?.collateralToken !== 0) throw new Error(`${dex} 不使用 USDC 保证金,暂不支持划转`)
915
+ const account = (value) => { const name = String(value ?? '').trim(); return name === '' || name === 'main' || name === 'core' ? 'perp' : name }
916
+ let from, to
917
+ if (request?.from != null || request?.to != null) { from = account(request?.from); to = account(request?.to) }
918
+ else {
919
+ const dex = String(request?.dex ?? '').trim()
920
+ if (!dex || dex.includes(':')) throw new Error('需要 from / to(spot、perp 或 HIP-3 交易所名),或 dex + direction')
921
+ ;[from, to] = request?.direction === 'out' ? [dex, 'perp'] : ['perp', dex]
922
+ }
923
+ if (from === to) throw new Error('from 和 to 不能相同')
924
+ if ([from, to].some((name) => name.includes(':'))) throw new Error('交易所名不含冒号,如 xyz')
919
925
  const amount = Number(request?.amount)
920
926
  if (!Number.isFinite(amount) || amount <= 0) throw new Error('amount 必须为正数(USDC)')
921
927
  if (Number(amount.toFixed(6)) !== amount) throw new Error('USDC 划转金额最多 6 位小数')
922
928
  if (amount > this.cfg.maxPositionUsd) throw new Error(`单次划转不超过仓位上限 ${this.cfg.maxPositionUsd} USD`)
929
+ // Every leg has the core perp account on one side; a route without it goes through perp.
930
+ const legs = from === 'perp' || to === 'perp' ? [[from, to]] : [[from, 'perp'], ['perp', to]]
931
+ const dexes = [...new Set([from, to].filter((name) => name !== 'spot' && name !== 'perp'))]
932
+ if (dexes.length) {
933
+ const known = await this.perpDexList()
934
+ for (const dex of dexes) {
935
+ if (!known.some((row) => row?.name === dex)) throw new Error(`未知的 HIP-3 交易所 ${dex}`)
936
+ const meta = await this.client.info('meta', { dex })
937
+ if (meta?.collateralToken !== 0) throw new Error(`${dex} 不使用 USDC 保证金,暂不支持划转`)
938
+ }
939
+ }
923
940
  const address = await this.resolveAddress()
924
941
  if (!address) throw new Error('live 划转需要钱包地址')
925
942
  const signerAddress = this.cfg.signer === 'web3' ? await this.web3Address() : await addressFromPrivateKey(this.cfg.privateKey)
926
- if (signerAddress.toLowerCase() !== address.toLowerCase()) throw new Error('保证金划转需要交易主钱包签名;当前签名钱包与 tradingAddress 不一致,API wallet 不能用于此划转')
927
- const source = toDex ? '' : dex
928
- const sourceState = source ? await this.client.info('clearinghouseState', { user: address, dex: source }) : await this.client.clearinghouseState(address)
929
- const withdrawable = Number(sourceState?.withdrawable)
930
- if (!Number.isFinite(withdrawable)) throw new Error('无法确认可划转余额')
931
- if (amount > withdrawable + 1e-9) throw new Error(`${source || '主永续账户'} 可划转 ${withdrawable.toFixed(2)} USDC,不足 ${amount}`)
932
- const spotMeta = await this.client.info('spotMeta')
933
- const usdc = (spotMeta?.tokens ?? []).find((token) => token.index === 0 && token.name === 'USDC')
934
- if (!usdc?.tokenId) throw new Error('无法读取 USDC 代币信息')
935
- const what = `${amount} USDC:${toDex ? `主永续账户 → ${dex}` : `${dex} → 主永续账户`}`
936
- if (!this.isManaged()) await this.authorizeToolAction(`Hyperliquid ${this.cfg.network} 划转 ${what}?`, exec, `Hyperliquid ${this.cfg.network}: transfer ${amount} USDC ${toDex ? `core perps → ${dex}` : `${dex} → core perps`}?`)
937
- if (transferEpoch !== this.walletEpoch) throw new Error('钱包已切换,划转已取消')
938
- const nonce = this.client.nextNonce()
939
- const { action, typed } = buildSendAsset({ destination: address, sourceDex: source, destinationDex: toDex ? dex : '', token: `USDC:${usdc.tokenId}`,
940
- amount: String(Number(amount.toFixed(6))), nonce, isMainnet: this.cfg.network === 'mainnet' })
941
- let signature
943
+ if (signerAddress.toLowerCase() !== address.toLowerCase()) throw new Error('资金划转需要交易主钱包签名;当前签名钱包与 tradingAddress 不一致,API wallet 不能用于划转')
944
+ const label = (name) => name === 'spot' ? '现货账户' : name === 'perp' ? '主永续账户' : name
945
+ const labelEn = (name) => name === 'spot' ? 'spot' : name === 'perp' ? 'core perps' : name
946
+ // Unified / portfolio-margin accounts already share spot and perp USDC: no spot ↔ perp leg. The mode
947
+ // must be known before signing: a failed lookup stops the transfer.
948
+ if (from === 'spot' || to === 'spot') {
949
+ const mode = await this.accountMode(address, true)
950
+ if (mode === 'unifiedAccount' || mode === 'portfolioMargin') throw new Error(`账户模式为 ${mode},现货 USDC 已直接作为保证金,现货与永续之间无需划转`)
951
+ }
952
+ // What can leave the source account now (the later leg spends what the first one brought into perp).
953
+ let available
954
+ if (from === 'spot') {
955
+ const spot = await this.client.spotClearinghouseState(address)
956
+ const usdc = (spot?.balances ?? []).find((row) => row.coin === 'USDC')
957
+ available = Number(usdc?.total ?? 0) - Number(usdc?.hold ?? 0)
958
+ } else {
959
+ const state = from === 'perp' ? await this.client.clearinghouseState(address) : await this.client.info('clearinghouseState', { user: address, dex: from })
960
+ available = Number(state?.withdrawable)
961
+ }
962
+ if (!Number.isFinite(available)) throw new Error('无法确认可划转余额')
963
+ if (amount > available + 1e-9) throw new Error(`${label(from)}可划转 ${Math.max(0, available).toFixed(2)} USDC,不足 ${amount}`)
964
+ const route = legs.length > 1 ? `${label(from)} → 主永续账户 → ${label(to)}(两步)` : `${label(from)} → ${label(to)}`
965
+ const what = `${amount} USDC:${route}`
966
+ if (!this.isManaged()) await this.authorizeToolAction(`Hyperliquid ${this.cfg.network} 划转 ${what}?`, exec,
967
+ `Hyperliquid ${this.cfg.network}: transfer ${amount} USDC ${[from, ...legs.map(([, next]) => next)].map(labelEn).join(' → ')}?`)
968
+ const isMainnet = this.cfg.network === 'mainnet'
969
+ const amountText = String(Number(amount.toFixed(6)))
970
+ let usdcToken = null
971
+ const responses = []
972
+ for (const [index, [source, target]] of legs.entries()) {
973
+ // Everything before the second leg is submitted can stop it; the first leg's money then sits in perp.
974
+ const parked = `第一步已完成(${amount} USDC 已到主永续账户)`
975
+ let submitted = false
976
+ try {
977
+ if (transferEpoch !== this.walletEpoch) throw new Error('钱包已切换,划转已取消')
978
+ exec.signal?.throwIfAborted()
979
+ if (index > 0) {
980
+ // The money that just arrived must be free to leave perp (positions may have taken it as margin).
981
+ const state = await this.client.clearinghouseState(address)
982
+ const free = Number(state?.withdrawable)
983
+ if (!Number.isFinite(free) || free + 1e-9 < amount) throw new Error(`主永续账户可划转 ${Number.isFinite(free) ? free.toFixed(2) : '未知'} USDC,不足 ${amount}`)
984
+ }
985
+ const nonce = this.client.nextNonce()
986
+ let built
987
+ if (source === 'spot' || target === 'spot') built = buildUsdClassTransfer({ amount: amountText, toPerp: target === 'perp', nonce, isMainnet })
988
+ else {
989
+ if (!usdcToken) {
990
+ const spotMeta = await this.client.info('spotMeta')
991
+ usdcToken = (spotMeta?.tokens ?? []).find((token) => token.index === 0 && token.name === 'USDC')?.tokenId
992
+ if (!usdcToken) throw new Error('无法读取 USDC 代币信息')
993
+ }
994
+ built = buildSendAsset({ destination: address, sourceDex: source === 'perp' ? '' : source, destinationDex: target === 'perp' ? '' : target, token: `USDC:${usdcToken}`, amount: amountText, nonce, isMainnet })
995
+ }
996
+ const signature = await this.signUserTyped(built.typed, `Hyperliquid ${this.cfg.network} 资金划转(${index + 1}/${legs.length}):${amount} USDC ${label(source)} → ${label(target)}`, exec, signerAddress)
997
+ // Cancelled while the wallet was signing: never submit that signature.
998
+ exec.signal?.throwIfAborted()
999
+ if (transferEpoch !== this.walletEpoch) throw new Error('钱包已切换,划转已取消')
1000
+ submitted = true
1001
+ const res = await this.client.exchangeUserSigned(built.action, signature, nonce)
1002
+ submitted = false
1003
+ assertExchangeAccepted(res)
1004
+ responses.push(res)
1005
+ } catch (error) {
1006
+ if (index === 0) throw error
1007
+ const reason = error?.message ?? String(error)
1008
+ this.store.recordEvent('live.transfer', { from: source, to: target, amount, failed: true, unknown: submitted, reason })
1009
+ // Submitted but no answer (timeout, network): it may have gone through. Never suggest a blind retry.
1010
+ if (submitted) throw new Error(`${parked};第二步 主永续账户 → ${label(target)} 已提交但未收到确认(${reason}),结果未知:先用 hyper_account action=state 核对主永续与 ${label(target)} 的余额,不要直接重试`)
1011
+ throw new Error(`${parked},第二步 主永续账户 → ${label(target)} 未完成:${reason}。资金在主永续账户,可稍后 from=perp to=${target} 补划`)
1012
+ }
1013
+ this.store.recordEvent('live.transfer', { from: source, to: target, amount, dex: [source, target].find((name) => name !== 'spot' && name !== 'perp'), toDex: target !== 'spot' && target !== 'perp' })
1014
+ }
1015
+ const dex = dexes.length === 1 ? dexes[0] : undefined
1016
+ return { ok: true, from, to, amount, legs: legs.map(([source, target]) => ({ from: source, to: target })), dex,
1017
+ direction: dex && legs.length === 1 ? (to === dex ? 'in' : 'out') : undefined, response: responses.at(-1), responses }
1018
+ }
1019
+
1020
+ /** Sign a user-signed action's EIP-712 payload with the main wallet (dsh-web3 or the local key). */
1021
+ async signUserTyped(typed, purpose, exec, signerAddress) {
942
1022
  if (this.cfg.signer === 'web3') {
943
1023
  const wallet = this.walletService()
944
1024
  if (!wallet) throw new Error('signer=web3 但 dsh-web3 未安装')
945
- const result = await wallet.signTypedData({ account: this.cfg.web3Account, ...typed, purpose: `Hyperliquid ${this.cfg.network} 资金划转:${what}` },
1025
+ const result = await wallet.signTypedData({ account: this.cfg.web3Account, ...typed, purpose },
946
1026
  this.approvalGateway(), { toolName: 'dsh-hyperliquid', agent: exec?.agent, callId: exec?.callId, signal: exec?.signal })
947
1027
  if (result?.status !== 'signed' || !result.signature) throw new Error(`dsh-web3 签名未完成(status=${result?.status})`)
948
1028
  if (result.address?.toLowerCase() !== signerAddress.toLowerCase()) throw new Error('dsh-web3 返回的签名地址与配置账户不一致')
949
- signature = splitSignature(result.signature)
950
- } else {
951
- if (!this.cfg.privateKey) throw new Error('live 划转需要钱包私钥')
952
- signature = await signTypedDataLocal(this.cfg.privateKey, typed)
1029
+ return splitSignature(result.signature)
953
1030
  }
954
- exec.signal?.throwIfAborted()
955
- const res = await this.client.exchangeUserSigned(action, signature, nonce)
956
- assertExchangeAccepted(res)
957
- this.store.recordEvent('live.transfer', { dex, amount, toDex })
958
- return { ok: true, dex, amount, direction: toDex ? 'in' : 'out', response: res }
1031
+ if (!this.cfg.privateKey) throw new Error('live 划转需要钱包私钥')
1032
+ return signTypedDataLocal(this.cfg.privateKey, typed)
959
1033
  }
960
1034
 
961
1035
  applyRiskSettings(limits, override) {
package/lib/signing.js CHANGED
@@ -247,6 +247,31 @@ export function buildSendAsset({ destination, sourceDex, destinationDex, token,
247
247
  }
248
248
  }
249
249
 
250
+ /** `usdClassTransfer` fields, as the official SDK signs them. */
251
+ export const USD_CLASS_TRANSFER_TYPES = [
252
+ { name: 'hyperliquidChain', type: 'string' },
253
+ { name: 'amount', type: 'string' },
254
+ { name: 'toPerp', type: 'bool' },
255
+ { name: 'nonce', type: 'uint64' },
256
+ ]
257
+
258
+ /**
259
+ * A `usdClassTransfer` action (USDC between the spot balance and the core perp account) and its EIP-712 payload.
260
+ * @returns {{ action, typed: { domain, types, value } }}
261
+ */
262
+ export function buildUsdClassTransfer({ amount, toPerp, nonce, isMainnet = true }) {
263
+ const action = { type: 'usdClassTransfer', signatureChainId: USER_SIGNATURE_CHAIN_ID, hyperliquidChain: isMainnet ? 'Mainnet' : 'Testnet', amount, toPerp: Boolean(toPerp), nonce }
264
+ const value = Object.fromEntries(USD_CLASS_TRANSFER_TYPES.map(({ name }) => [name, action[name]]))
265
+ return {
266
+ action,
267
+ typed: {
268
+ domain: { name: 'HyperliquidSignTransaction', version: '1', chainId: parseInt(USER_SIGNATURE_CHAIN_ID, 16), verifyingContract: '0x0000000000000000000000000000000000000000' },
269
+ types: { 'HyperliquidTransaction:UsdClassTransfer': USD_CLASS_TRANSFER_TYPES },
270
+ value,
271
+ },
272
+ }
273
+ }
274
+
250
275
  /** EIP-712 digest + secp256k1 signature for a single-struct payload of string / uint / bool / address fields. */
251
276
  export async function signTypedDataLocal(privateKey, { domain, types, value }) {
252
277
  const c = await loadCrypto()
package/lib/tools.js CHANGED
@@ -356,7 +356,7 @@ function buildTools(remote) {
356
356
  description: '查询交易账户。action=state 返回权益/持仓;orders 返回挂单;fills 返回成交;orderStatus 用 oid 或 cloid 核对某笔 live 订单,尤其在网络超时后。review 返回最近 7/30/90 天成交复盘、订单数量核对、费用和资金费覆盖缺口;不是完整账户收益率。' +
357
357
  'action=analyze 交易体检(默认 90 天):按完整往返统计胜率、盈亏比、期望、持仓时长、按币种/时段/星期的表现、手续费与资金费,并给出按金额排序的习惯问题和建议规则——第一次见面或用户问“我做得怎么样”时用它,不要自己从成交里手算。' +
358
358
  'action=limits 修改允许币对(addCoins/removeCoins)与单笔/每日/仓位/杠杆上限——仅全托管模式可用,半托管时由用户在 设置 → 交易限额与签名规则 修改。' +
359
- 'action=transfer 在主永续账户与 HIP-3 交易所之间划转 USDC(dex,amount,direction=in 转入 / out 转回)——全托管直接执行,半托管每次需用户批准;仅 live。',
359
+ 'action=transfer 在自己的任意两个账户之间划转 USDC:from / to 取 spot(现货)、perp(主永续)或 HIP-3 交易所名(如 xyz),amount 为数量;spot↔perp、perp↔HIP-3 一步完成,spot↔HIP-3 与 HIP-3↔HIP-3 自动经 perp 两步完成(只需批准一次)。下单前永续保证金不足而现货有 USDC 时,先 spot→perp。统一账户 / 组合保证金模式无需 spot↔perp 划转。全托管直接执行,半托管每次需用户批准;仅 live,需交易主钱包签名。旧参数 dex + direction(in/out)仍可用。',
360
360
  parameters: {
361
361
  type: 'object',
362
362
  properties: {
@@ -364,9 +364,11 @@ function buildTools(remote) {
364
364
  addCoins: { type: 'array', items: { type: 'string' }, description: 'action=limits:加入允许币对,如 ["xyz:XYZ100"]' },
365
365
  removeCoins: { type: 'array', items: { type: 'string' }, description: 'action=limits:移出允许币对' },
366
366
  maxOrderUsd: { type: 'number' }, maxDailyUsd: { type: 'number' }, maxPositionUsd: { type: 'number' }, maxLeverage: { type: 'number' },
367
- dex: { type: 'string', description: 'action=transfer:HIP-3 交易所名,如 xyz' },
367
+ from: { type: 'string', description: 'action=transfer:转出账户,spot / perp / HIP-3 交易所名' },
368
+ to: { type: 'string', description: 'action=transfer:转入账户,spot / perp / HIP-3 交易所名' },
369
+ dex: { type: 'string', description: 'action=transfer(旧写法):HIP-3 交易所名,配合 direction' },
368
370
  amount: { type: 'number', description: 'action=transfer:USDC 数量' },
369
- direction: { type: 'string', enum: ['in', 'out'], description: 'action=transfer:in=主账户转入该交易所(默认),out=转回主账户' },
371
+ direction: { type: 'string', enum: ['in', 'out'], description: 'action=transfer(旧写法):in=主永续转入 dex(默认),out=转回主永续' },
370
372
  oid: { type: 'string', description: 'orderStatus 必填;交易所 oid 或 0x 开头 128 位 cloid' },
371
373
  days: { type: 'number', enum: [7, 30, 90], description: 'review 窗口,默认 30 天' },
372
374
  coin: { type: 'string', description: 'review 可选,仅核对该标的;省略为全部' },
@@ -394,7 +396,8 @@ function buildTools(remote) {
394
396
  }
395
397
  if (action === 'transfer') {
396
398
  const res = await remote.transferCollateral(args, exec)
397
- return { text: `已划转 ${res.amount} USDC:${res.direction === 'in' ? `主永续账户 → ${res.dex}` : `${res.dex} → 主永续账户`}`, ...res }
399
+ const name = (value) => value === 'spot' ? '现货账户' : value === 'perp' ? '主永续账户' : value
400
+ return { text: `已划转 ${res.amount} USDC:${[res.from, ...res.legs.map((leg) => leg.to)].map(name).join(' → ')}${res.legs.length > 1 ? '(两步)' : ''}`, ...res }
398
401
  }
399
402
  if (action === 'state') {
400
403
  const a = await remote.account({ coin: remote.desk?.coin })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hyperliquid",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Hyperliquid trading desk for the DeepSeek Harness (dsh) - market workspace, native agent conversation and guarded live / paper trading over the Hyperliquid API.",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",
@@ -34,7 +34,7 @@ description: 作为用户的交易副手,在 DSH 交易会话中用 Hyperliqui
34
34
  |---|---|---|
35
35
  | 实盘下单、撤单、改杠杆 | 每笔都需要用户批准 | 在限额内直接执行 |
36
36
  | 允许币对与单笔/每日/仓位/杠杆上限 | 只能由用户在 **设置 → 交易限额与签名规则** 修改 | 你可以用 `hyper_account action=limits` 调整(`addCoins` / `removeCoins` / `maxOrderUsd` / `maxDailyUsd` / `maxPositionUsd` / `maxLeverage`) |
37
- | HIP-3 保证金划转 | `hyper_account action=transfer`,每次需要用户批准 | 直接执行 |
37
+ | USDC 划转(现货、主永续、各 HIP-3 交易所之间任意互转) | `hyper_account action=transfer`,每次需要用户批准 | 直接执行 |
38
38
 
39
39
  两种模式下 dsh-web3 钱包自己的签名策略都仍然生效:钱包拒签就停止,不要绕过。
40
40
 
@@ -43,11 +43,18 @@ description: 作为用户的交易副手,在 DSH 交易会话中用 Hyperliqui
43
43
  - 不要拆单来绕过任何限额。
44
44
  - **网络(mainnet/testnet)和模式(paper/live)**由用户在设置里切换。插件**不做提现**。
45
45
 
46
+ ### 现货与永续的 USDC 是分开的
47
+ 默认账户模式(`hyper_context.accountMode` 为 `default`)下,现货 USDC 和永续保证金是两个余额:现货里有钱、永续可用保证金不够时,下单会被拒。
48
+ - 先确认:`hyper_account action=state` 看永续可用保证金;现货余额在 Hyperliquid 现货账户里。
49
+ - 不够时用 `hyper_account action=transfer from=spot to=perp amount=…` 把这笔交易需要的保证金加一点缓冲划到永续;不再交易时可 `from=perp to=spot` 划回。半托管需要用户批准,全托管直接执行。不要让用户去网页手动划转。
50
+ - 统一账户(`unifiedAccount`)/ 组合保证金(`portfolioMargin`)模式下现货 USDC 直接作保证金,不需要也不能这样划转。
51
+ - 现货 ↔ HIP-3、HIP-3 ↔ HIP-3 直接写 `from=spot to=xyz` / `from=xyz to=abc`,插件自动经主永续两步完成,只需批准一次;如果第二步失败,报错会说明钱已停在主永续,再单独 `from=perp to=…` 补一步即可。
52
+
46
53
  ### HIP-3 的保证金是独立的
47
54
  每个 HIP-3 交易所(如 xyz)都有独立的保证金账户,和主永续账户的余额不互通:
48
- - **Standard 账户**:开仓前,该交易所里要有 USDC。不够时用 `hyper_account action=transfer dex=xyz amount=… direction=in` 从主永续账户划入(半托管需要用户批准,全托管直接执行)。只划入这笔交易需要的保证金加一点缓冲,不要整笔搬空主账户。平仓后不再交易该市场时,可以用 `direction=out` 转回。用户也可以自己在 Hyperliquid 网页划转。
55
+ - **Standard 账户**:开仓前,该交易所里要有 USDC。不够时用 `hyper_account action=transfer from=perp to=xyz amount=…`(旧写法 `dex=xyz direction=in` 仍可用)从主永续账户划入(半托管需要用户批准,全托管直接执行)。只划入这笔交易需要的保证金加一点缓冲,不要整笔搬空主账户。平仓后不再交易该市场时,可以用 `from=xyz to=perp` 转回。
49
56
  - **统一账户(Unified Account)/ Portfolio Margin**:下 HIP-3 单时会自动调用主账户的 USDC,不需要划转。`hyper_context.accountMode` 能看出当前是哪种。开启统一账户需要用户在 Hyperliquid 网页操作。
50
- - 划转是主钱包签名的 `sendAsset` 动作,只支持以 USDC 为保证金的交易所,单次不超过仓位上限,并且只在 live 模式下可用。
57
+ - 划转都由交易主钱包签名(现货 ↔ 永续是 `usdClassTransfer`,永续 ↔ HIP-3 是 `sendAsset`),HIP-3 只支持以 USDC 为保证金的交易所;单次不超过仓位上限,只在 live 模式下可用。
51
58
  - HIP-3 的手续费、杠杆上限与核心永续不同(按市场读取 `maxLeverage`),部分市场只允许逐仓。
52
59
 
53
60
  ### 执行能力与限制