repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | /** |
| #2 | * leviathan/src/agent/vulcan.ts — Typed wrapper for vulcan-cli (Phoenix perps) |
| #3 | * |
| #4 | * Devnet-only enforcement is baked in. Every command injects --url=devnet |
| #5 | * unless the VULCAN_MAINNET env var is explicitly set to '1' (requires human). |
| #6 | * |
| #7 | * Live order placement requires BOTH: |
| #8 | * LIVE_TRADING=true AND OPERATOR_CONFIRMED=true |
| #9 | * If either flag is absent the placeOrder call returns a [PAPER TRADE] result |
| #10 | * and NEVER touches the chain. |
| #11 | * |
| #12 | * Commands map to the vulcan CLI: `vulcan <command> [options]` |
| #13 | */ |
| #14 | |
| #15 | import { execa } from 'execa'; |
| #16 | |
| #17 | const VULCAN_TIMEOUT = 15_000; |
| #18 | |
| #19 | export function resolveCluster(): string { |
| #20 | if (process.env['VULCAN_MAINNET'] === '1') return 'mainnet-beta'; |
| #21 | return 'devnet'; |
| #22 | } |
| #23 | |
| #24 | // ─── Response shapes ───────────────────────────────────────────────────────── |
| #25 | |
| #26 | export interface VulcanMarket { |
| #27 | pubkey: string; |
| #28 | name: string; |
| #29 | baseSymbol: string; |
| #30 | quoteSymbol: string; |
| #31 | openInterest: string; |
| #32 | fundingRate: string; |
| #33 | markPrice: string; |
| #34 | } |
| #35 | |
| #36 | export interface VulcanQuote { |
| #37 | market: string; |
| #38 | side: 'long' | 'short'; |
| #39 | size: string; |
| #40 | entryPrice: string; |
| #41 | fee: string; |
| #42 | liquidationPrice: string; |
| #43 | notional: string; |
| #44 | } |
| #45 | |
| #46 | export interface VulcanOrderResult { |
| #47 | orderId: string; |
| #48 | market: string; |
| #49 | side: 'long' | 'short'; |
| #50 | size: string; |
| #51 | price: string; |
| #52 | fee: string; |
| #53 | signature?: string; |
| #54 | paperMode?: boolean; |
| #55 | } |
| #56 | |
| #57 | export interface VulcanPosition { |
| #58 | positionId: string; |
| #59 | market: string; |
| #60 | side: 'long' | 'short'; |
| #61 | size: string; |
| #62 | entryPrice: string; |
| #63 | markPrice: string; |
| #64 | unrealizedPnl: string; |
| #65 | liquidationPrice: string; |
| #66 | } |
| #67 | |
| #68 | export interface VulcanFundingRate { |
| #69 | market: string; |
| #70 | fundingRate: string; |
| #71 | nextFundingTime: string; |
| #72 | direction: 'longs_pay_shorts' | 'shorts_pay_longs'; |
| #73 | } |
| #74 | |
| #75 | export interface VulcanAccountInfo { |
| #76 | wallet: string; |
| #77 | equity: string; |
| #78 | freeMargin: string; |
| #79 | usedMargin: string; |
| #80 | leverage: string; |
| #81 | marginRatio: string; |
| #82 | } |
| #83 | |
| #84 | export interface VulcanPlaceOrderOpts { |
| #85 | /** Limit price — omit for market orders */ |
| #86 | limitPrice?: string; |
| #87 | /** Reduce-only order (close position only) */ |
| #88 | reduceOnly?: boolean; |
| #89 | /** Client-side order tag */ |
| #90 | clientOrderId?: string; |
| #91 | } |
| #92 | |
| #93 | // ─── Internal runner ───────────────────────────────────────────────────────── |
| #94 | |
| #95 | async function runCmd( |
| #96 | args: string[], |
| #97 | timeoutMs = VULCAN_TIMEOUT, |
| #98 | ): Promise<{ stdout: string; success: boolean; error?: string }> { |
| #99 | const cluster = resolveCluster(); |
| #100 | const fullArgs = [...args, `--url=${cluster}`, '--output=json']; |
| #101 | try { |
| #102 | const result = await execa('vulcan', fullArgs, { timeout: timeoutMs }); |
| #103 | return { stdout: result.stdout, success: true }; |
| #104 | } catch (e: unknown) { |
| #105 | const msg = e instanceof Error ? e.message : String(e); |
| #106 | return { stdout: '', success: false, error: msg.slice(0, 200) }; |
| #107 | } |
| #108 | } |
| #109 | |
| #110 | // ─── VulcanClient ───────────────────────────────────────────────────────────── |
| #111 | |
| #112 | export const VulcanClient = { |
| #113 | /** |
| #114 | * List all Phoenix perp markets. |
| #115 | */ |
| #116 | async markets(): Promise<VulcanMarket[] | string> { |
| #117 | const r = await runCmd(['markets']); |
| #118 | if (!r.success) return `vulcan markets failed: ${r.error}`; |
| #119 | try { return JSON.parse(r.stdout) as VulcanMarket[]; } |
| #120 | catch { return r.stdout.slice(0, 500); } |
| #121 | }, |
| #122 | |
| #123 | /** |
| #124 | * Get a trade quote (entry price, fees, liquidation price) before committing. |
| #125 | * Always safe — never executes anything on-chain. |
| #126 | */ |
| #127 | async quote( |
| #128 | market: string, |
| #129 | side: 'long' | 'short', |
| #130 | size: string, |
| #131 | ): Promise<VulcanQuote | string> { |
| #132 | const r = await runCmd(['quote', '--market', market, `--side=${side}`, `--size=${size}`]); |
| #133 | if (!r.success) return `vulcan quote failed: ${r.error}`; |
| #134 | try { return JSON.parse(r.stdout) as VulcanQuote; } |
| #135 | catch { return r.stdout.slice(0, 500); } |
| #136 | }, |
| #137 | |
| #138 | /** |
| #139 | * Place a perp order. |
| #140 | * |
| #141 | * SAFETY GATE: returns a [PAPER TRADE] result unless BOTH |
| #142 | * process.env.LIVE_TRADING === 'true' |
| #143 | * process.env.OPERATOR_CONFIRMED === 'true' |
| #144 | * are set. Live execution is NEVER triggered without both flags. |
| #145 | * |
| #146 | * Note: wallet signing is handled by the vulcan keystore / --wallet env var; |
| #147 | * no private key material is ever passed through this function. |
| #148 | */ |
| #149 | async placeOrder( |
| #150 | market: string, |
| #151 | side: 'long' | 'short', |
| #152 | size: string, |
| #153 | opts: VulcanPlaceOrderOpts = {}, |
| #154 | ): Promise<VulcanOrderResult | string> { |
| #155 | // ── CRITICAL safety gate ──────────────────────────────────────────────── |
| #156 | const liveTrading = process.env['LIVE_TRADING'] === 'true'; |
| #157 | const operatorConfirmed = process.env['OPERATOR_CONFIRMED'] === 'true'; |
| #158 | |
| #159 | if (!liveTrading || !operatorConfirmed) { |
| #160 | // Paper trade: fetch a quote and return it without broadcasting |
| #161 | const quoteResult = await VulcanClient.quote(market, side, size); |
| #162 | if (typeof quoteResult === 'string') { |
| #163 | return `[PAPER TRADE] quote failed — ${quoteResult}`; |
| #164 | } |
| #165 | return { |
| #166 | orderId: `paper-${Date.now()}`, |
| #167 | market, |
| #168 | side, |
| #169 | size, |
| #170 | price: quoteResult.entryPrice, |
| #171 | fee: quoteResult.fee, |
| #172 | paperMode: true, |
| #173 | }; |
| #174 | } |
| #175 | |
| #176 | // ── Live path (both flags confirmed) ──────────────────────────────────── |
| #177 | const args = ['place-order', '--market', market, `--side=${side}`, `--size=${size}`]; |
| #178 | if (opts.limitPrice) args.push(`--limit-price=${opts.limitPrice}`); |
| #179 | if (opts.reduceOnly) args.push('--reduce-only'); |
| #180 | if (opts.clientOrderId) args.push(`--client-order-id=${opts.clientOrderId}`); |
| #181 | |
| #182 | const r = await runCmd(args); |
| #183 | if (!r.success) return `vulcan place-order failed: ${r.error}`; |
| #184 | try { return JSON.parse(r.stdout) as VulcanOrderResult; } |
| #185 | catch { return r.stdout.slice(0, 500); } |
| #186 | }, |
| #187 | |
| #188 | /** |
| #189 | * Cancel an open order by its order ID. |
| #190 | */ |
| #191 | async cancelOrder(orderId: string): Promise<unknown> { |
| #192 | const r = await runCmd(['cancel-order', '--order-id', orderId]); |
| #193 | if (!r.success) return `vulcan cancel-order failed: ${r.error}`; |
| #194 | try { return JSON.parse(r.stdout); } |
| #195 | catch { return r.stdout.slice(0, 500); } |
| #196 | }, |
| #197 | |
| #198 | /** |
| #199 | * List open positions for a wallet (defaults to the configured keystore wallet). |
| #200 | */ |
| #201 | async positions(wallet?: string): Promise<VulcanPosition[] | string> { |
| #202 | const args = ['positions']; |
| #203 | if (wallet) args.push('--wallet', wallet); |
| #204 | const r = await runCmd(args); |
| #205 | if (!r.success) return `vulcan positions failed: ${r.error}`; |
| #206 | try { return JSON.parse(r.stdout) as VulcanPosition[]; } |
| #207 | catch { return r.stdout.slice(0, 500); } |
| #208 | }, |
| #209 | |
| #210 | /** |
| #211 | * Get the current funding rate for a Phoenix perp market. |
| #212 | */ |
| #213 | async fundingRate(market: string): Promise<VulcanFundingRate | string> { |
| #214 | const r = await runCmd(['funding-rate', '--market', market]); |
| #215 | if (!r.success) return `vulcan funding-rate failed: ${r.error}`; |
| #216 | try { return JSON.parse(r.stdout) as VulcanFundingRate; } |
| #217 | catch { return r.stdout.slice(0, 500); } |
| #218 | }, |
| #219 | |
| #220 | /** |
| #221 | * Get account equity, margin, and leverage for a wallet. |
| #222 | */ |
| #223 | async accountInfo(wallet?: string): Promise<VulcanAccountInfo | string> { |
| #224 | const args = ['account']; |
| #225 | if (wallet) args.push('--wallet', wallet); |
| #226 | const r = await runCmd(args); |
| #227 | if (!r.success) return `vulcan account failed: ${r.error}`; |
| #228 | try { return JSON.parse(r.stdout) as VulcanAccountInfo; } |
| #229 | catch { return r.stdout.slice(0, 500); } |
| #230 | }, |
| #231 | }; |
| #232 |