mirror of
https://github.com/michaelshimeles/terminal.git
synced 2026-11-07 05:56:17 -10:00
* docs: design for Hyperliquid trading (crypto + HIP-3 stock perps) Design only, no feature code. Covers custody (client-side API wallet signing, recommended), agent approve/revoke flow, asset routing for core/spot/HIP-3, order ticket, user streams for POS/ORD/ACTV, safety rails, testnet-only test plan, phases, and open questions including the Hyperliquid ToS restriction on Ontario. * docs(hl-trading): plan the market key so HL perp rows reprice and trade Greptile: market_key() returns None for Future and live_price/row actions depend on it. Add Position.market and the portfolio_view work. * docs(hl-trading): keep testnet and mainnet quotes in separate maps Greptile: same market keys on both networks would let one overwrite the other in the shared quote map. Plan a network-aware live_price and mainnet-only gateway requests, with interleaved-update tests. * feat(hl): add tr-hl, a testnet-only Hyperliquid signing and trading crate tr-hl signs and sends Hyperliquid actions from the device. It covers L1 phantom-agent signing and user-signed approveAgent, the order, cancel, cancelByCloid, modify, updateLeverage, scheduleCancel and noop actions, asset ids read from perpDexs and meta, tick and lot rounding, info and exchange clients, a user-stream reducer and a nonce manager. Only testnet exists in this crate. The endpoints are the testnet hosts, the phantom agent source is always "b", hyperliquidChain is always "Testnet", and Network has one variant. A test greps the crate for mainnet hosts. The caps are constants and the trader enforces them before it signs. Orders are capped at $100 notional, leverage at 3x and market slippage at 1%. The builder field is never sent. Signatures match the Python SDK's published vectors, plus an approveAgent vector generated with SDK 0.24.0. The crate builds for wasm32. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat: Hyperliquid testnet trading in the app Hyperliquid joins LIVE next to SnapTrade, with its own opt-in in Settings > Trading > Hyperliquid and an amber TESTNET badge on the toolbar, the account, the ticket and every confirmation. - Connect an address read-only. The account's positions, orders, fills and funding then show in PORT, POS, ORD and ACTV, from Hyperliquid's websocket and never through Terminal's server. - Approve an agent on the desktop by pasting a throwaway testnet main key once. Terminal wipes it after signing and never stores it. In the browser the wallet signs instead (EIP-1193). The agent key lives in the macOS Keychain or the Linux Secret Service, and in browser memory or, by opt-in, IndexedDB under a non-extractable WebCrypto key. There is no file fallback. - The ticket's Hyperliquid variant covers market (IOC, 1% cap), limit (GTC, IOC, post-only), stop loss and take profit, reduce-only and "Set 3x" leverage. HIP-3 rows read "PERP · HL xyz (perpetual, not a share)". - Every order, cancel, cancel-all and leverage change asks for one confirmation. The worker re-reads the testnet mid and leverage and checks the caps again right before it signs. - KILL and a button in ORD cancel every open order and turn Hyperliquid orders off. The dead man's switch is opt-in. - Testnet rows price only from testnet mids. Position.market now carries the exact key, and live_price picks the map by network. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): mask addresses in Hyperliquid messages, toasts and logs Hyperliquid's errors name full addresses ("User or API Wallet 0x… does not exist", "Must deposit… User: 0x…"). mask_addresses turns each one into 0x1234…abcd. Exchange summaries, the worker's logs and every app message that shows a Hyperliquid error now go through it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): keep Hyperliquid out of SnapTrade's settings sections The offscreen render showed the Hyperliquid account under SnapTrade's Brokerage access, with SnapTrade's "reconnect with trading access" advice. That section and the default-account picker now list SnapTrade accounts only. Hyperliquid has its own section. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(hl-trading): record the v1 decisions and add screenshots and a wasm CI check Section 15 of the design now records Micky's twelve decisions as "Decided (2026-11-02)". The sections they change are updated: testnet only, Secret Service with no file fallback on Linux, a pasted throwaway key on the desktop for now, hard caps, an opt-in dead man's switch and the kill switch. A new section 16 lists what v1 builds and what is still missing. docs/img/hl-*.png holds the before (origin/master) and after screenshots. CI now builds and lints tr-hl for wasm32. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): read the agent key from the store once per account When the store had no key for the approved agent, the app asked again on every frame, which meant a Keychain or Secret Service read per frame. It now remembers the account it read and asks again only after a connect, an approval or a revoke. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): fix the kill switch, uncertain sends, approvals and cap valuation Fixes from Greptile's first review of #37: - Totals value a margined position (a perp) by its P&L, not its notional. Hyperliquid's cash is its collateral (equity less unrealised P&L), so a reprice moves equity only by the P&L change. - KILL reads the open orders fresh on every dex, cancels them, and reads again (three rounds at most) before it reports what is left. Signed actions now run one at a time, and an order confirmed before a KILL that hasn't been signed yet is refused. - An approval answer applies only to the account that asked for it, and Remove is disabled while one is in flight. The app also never signs with a key that isn't the approved agent's. - An uncertain send blocks new orders until orderStatus finds its cloid, or until its 30 s expiresAfter window has passed without it. - Every order needs a current mid before it is valued against the $100 cap. Prices that round to zero are refused, and rounding zero no longer overflows. - The keyring check runs in the worker, not on the UI thread. - A cancel-all skips a market the registry doesn't know instead of failing, and the pasted key field's undo history is wiped once the key has signed. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): keep the dead man's switch armed while an order is unresolved From Greptile's second review of #37: an order with no clear answer blocked new sends through the same guard the dead man's switch reads, so the switch cleared its scheduled cancel just when a possibly accepted order needed it. The guard is now split. Whether this device can sign arms the switch, and allows cancels and leverage changes. Only new orders and modifies also wait for an unresolved order. KILL always goes ahead. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): size dollar amounts at the mid and let KILL skip stalled requests Fixes from Greptile's fourth review of #37 (atf636b7c): - A dollar amount on the Hyperliquid ticket (BUY xyz:INTC $10) became that many contracts. The ticket now sizes it at the testnet mid, rounded down to the market's size step. With no mid yet, or under one step, it stays a dollar amount and the ticket can't send it. Changing the market sizes the amount again. - Every /info and /exchange request has its own 10 s deadline. The browser's fetch had none, so a stalled request could hold the worker's only turn forever. - KILL no longer waits for that turn. It cancels at once, and an order confirmed before it gives up, even one still reading the market. If an order was already being sent, KILL cancels again once its answer comes, or once its expiresAfter has passed if the send got no clear answer. Single cancels and the dead man's switch don't wait for orders either. Orders, modifies and leverage changes still go one at a time, and an order with no clear answer still blocks new ones until it is found or expires. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): size dollars for the market named now and keep timed cancels in order Fixes from Greptile's review off1ff1b3: - The ticket reads its market at the start of a frame, before an edit to the market field. A dollar amount could then be sized at the old market's mid ($10 at a $2 mid is 5 units, worth $20 on a $4 market). settle_size now sizes only with the market the ticket names, so the next frame sizes it for the new one. The ticket's estimate also uses that market. - Dead man's switch changes ran side by side, so a slow clear could land after a newer schedule and remove it. They now go out one at a time in the order the app made them, on a lock of their own that never waits for orders. A change that reaches the worker after a newer one is dropped. After a change with no clear answer, the next waits until it can no longer land. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): give each dead man's switch refresh a fresh deadline From Greptile's review of2f7cab6: after a change with no clear answer, every later change waited until that one could no longer land. Refreshes queue every 30 s, so with lost replies they went out every 35 s or so, each with the deadline it got when queued, and the timer could fire while Terminal was still running. - ScheduleCancel now carries how far ahead to cancel. The worker adds it to the time of signing, so a refresh that waited still gets a fresh deadline. - A newer change replaces one still waiting for its turn, so a backlog collapses into the newest. - Only a change of the other kind waits out a lost one: a timer after a clear, or a clear after a timer. A refresh that lands late only moves the timer back by one refresh, so refreshes don't wait for each other. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): make every dead man's switch change wait out a lost one From Greptile's review of73b71b8: letting a refresh go out before an earlier timer with a lost reply could no longer land was not safe. That timer could land last (the clocks may differ by seconds) and pull the deadline in, so the switch could fire while Terminal was running. Every change now waits until a lost earlier change can no longer land, whichever kind either one is. That is safe now that each change is signed, with its deadline, when it goes out, and a newer change replaces any still waiting, so a backlog can't build up stale deadlines. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(hl): sign approveAgent through a built-in Turnkey wallet, testnet only Add tr-turnkey, the client side of Terminal's built-in wallets. The device makes a P-256 key, and every Turnkey request carries a stamp made with it. The session token Turnkey issues for that key lasts 15 minutes, and Terminal accepts it only if it is bound to its own key. The client reads whoami and the wallet's Ethereum account, and signs one digest with the request alloy-signer-turnkey sends (SIGN_RAW_PAYLOAD_V2, hex payload, no hashing). On the desktop the browser returns the session to a listener on 127.0.0.1 that checks a 256-bit state. A mock feature adds a local stand-in for Turnkey's API and sign-in page for tests and screenshots. tr-hl gets sign_approve_agent_with, the only way a remote wallet's signature enters the crate. The signer sees only the digest of a testnet approveAgent, and the answer is used only if it recovers the wallet. approve_agent_hash now also refuses every signatureChainId except 0x66eee. The mock wallet holds the Python SDK's public test key and reproduces the SDK's approveAgent vector. Mainnet and 0xa4b1 approvals never reach the wallet, and the new code names no mainnet host. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(hl): design built-in Turnkey wallets for testnet trading Record Micky's decision of 2026-11-04 in §15: built-in wallets only, no browser wallet and no pasted key. The new §4.5 covers who holds which key, the browser sign-in that hands a session to a key made on the device, why a loopback redirect beats a terminal:// link, what Turnkey signs and how often, and the export through Turnkey's iframe. It explains why no Terminal server needs a Turnkey key (the Auth Proxy holds the one that creates sub-organizations) and sketches a scoped signup function as an unbuilt fallback. Agents now last 180 days by default, since each approval is a paid Turnkey signature. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(hl): replace the browser wallet and pasted key with the built-in wallet Settings › Trading › Hyperliquid now starts with "Create Wallet / Sign In". On the desktop it opens Terminal's sign-in page in the system browser and waits on a 127.0.0.1 loopback for the session; the web build opens the page in a popup and takes its postMessage. Terminal reads the wallet's address from Turnkey with requests stamped by its own key, keeps the 15-minute session in the OS secret store (memory on the web) and deletes it once it expires. "Approve Trading Key" makes the agent key on the device and has the wallet sign approveAgent through Turnkey, signing in first if there is no session. Revoke goes the same way. Agents now last 180 days by default, since each approval is a paid Turnkey signature. Orders are still signed only by the agent key. Deposit shows the address in full with a QR code and a copy button, the testnet perp account value and withdrawable from the stream, and spot USDC (new spotClearinghouseState read). Back Up Wallet warns, then opens the page's export mode. The web's EIP-1193 Connect Wallet and the desktop's pasted testnet key are gone. MainKey is left for tests and the testnet_check example. Watching an address read-only stays. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(site): add the wallet sign-in and backup page /wallet/ signs the user in with an email code or a passkey through @turnkey/core and Turnkey's Auth Proxy. A new user gets a sub-organization with one Ethereum wallet. The page then calls stamp_login for the app's public key, so the app gets a 15-minute session only its own key can use, and logs itself out. Desktop answers go to the loopback port, web answers to the opener with postMessage on the same origin. Its parameters come from the URL fragment, which never reaches a server, analytics or a Referer, and the page clears it on load. In export mode the page shows the recovery phrase in Turnkey's export iframe, so the phrase never touches the page's code or any server. It needs two public IDs in the Vercel project env and says so until they are set. @turnkey/core is pinned to 2.13.1 and @turnkey/iframe-stamper to 2.11.1. Co-Authored-By: Claude Opus 5.5 <[email protected]> * ci: build and lint tr-turnkey for wasm32 The browser build uses the wallet crate too, so check it the way tr-hl is checked. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): keep the trading key when the wallet doesn't sign a revoke Revoke used to forget the agent before Turnkey signed anything, so a failed or expired sign-in left the app without its agent while the message said nothing changed. Orders now turn off at once, and the agent is forgotten only when the revoke goes through. An approval the wallet didn't sign now says so instead of reading as Hyperliquid's refusal. The mock-URL warnings no longer print the URL. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): link the sign-in page at /wallet, its canonical URL The site serves the page at /wallet and redirects /wallet/ there. Browsers keep the fragment across that redirect, but there is no reason to rely on it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(site): only sign in, never sign up, when backing up a wallet Export mode hid nothing: it offered "Create with a passkey", and the email code path would create a new wallet for an unknown address. Backing up now logs in to an existing wallet only. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: add before and after screenshots for built-in wallets Desktop shots are offscreen renders of the running app against a local Turnkey stand-in. Web shots come from headless Chromium on a local build. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(hl): keep sign-in answers and wallet actions on the right wallet From Greptile on #38: - Removing the wallet reset the request counter, so a cancelled sign-in's late answer could carry the next sign-in's id and end it. Ids now keep counting, and removing the wallet cancels a sign-in in progress. - A revoke or approval that needed a sign-in acted on whichever wallet the user signed in to. The sign-in now remembers the wallet the action is for, and refuses another one without changing anything. - The browser build's sign-in never timed out. Both builds now give up after 10 minutes, the limit the waiting line shows. - The sign-in page stayed on "Connecting your wallet" if Turnkey didn't issue the device's session. It now goes back to the sign-in controls with the error, so the user can retry or cancel. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>