diff --git a/apps/docs/content/docs/clients.md b/apps/docs/content/docs/clients.md index 8bac0d04..6efc2d30 100644 --- a/apps/docs/content/docs/clients.md +++ b/apps/docs/content/docs/clients.md @@ -770,3 +770,41 @@ local subscribers in arrival order. Each callback receives its own mutable asset so changes made by one subscriber do not affect another. Unsubscribing or aborting suppresses queued deliveries; listeners added later do not receive previously queued frames. A corrupt frame or throwing callback does not end other listeners or stop subsequent updates. Separate transports keep separate decode queues. + +## Canonical actions and explicit execution + +Build an action once when its fields are stable, then choose when to sign and submit it: + +```ts +import { ExchangeClient } from "@bloxwap/hyperliquid/api/exchange/client"; +import { HttpTransport } from "@bloxwap/hyperliquid/transport/http"; +import { buildOrder } from "@bloxwap/hyperliquid/actions/order"; +import { privateKeyToAccount } from "viem/accounts"; + +const exchange = new ExchangeClient({ + transport: new HttpTransport(), + wallet: privateKeyToAccount(process.env.HL_PRIVATE_KEY as `0x${string}`), +}); +const action = buildOrder({ + orders: [{ a: 0, b: true, p: "30000", s: "0.01", r: false, t: { limit: { tif: "Gtc" } } }], +}); +const signed = await exchange.sign(action); +const result = await exchange.submit(signed); +// Or sign and submit in one coordinated call: +await exchange.execute(action); +``` + +Builders validate, normalize, fill defaults, and copy/freeze nested fields. They allocate no nonce and perform no +wallet or transport calls. The input remains owned by the caller. Reuse a built action while its fields remain valid; +resolve coin symbols to asset IDs before building. Time-dependent constraints such as scheduled cancellation are +checked when building, so rebuild those actions before reuse. +An explicit `buildNoop({ nonce })` retains that nonce rather than allocating a fresh one on reuse. + +Signing consumes one nonce and produces an immutable signed request. Submission preserves the operation's response +type and does not sign again. Submit promptly: expiration, the protocol timestamp range, and the signer's 100-highest +nonce window still apply. Signed ownership and network checks are in-process; serialize for storage only if you intend +to use the legacy `submitPrepared` wire-payload API. Reconstructed canonical actions must be rebuilt through a builder. + +The standalone `signAction`, `submitAction`, and `executeAction` functions in +`@bloxwap/hyperliquid/actions/execution` accept the same exchange config. Existing client methods and +`prepareRequest` / `submitPrepared` continue to work. diff --git a/apps/docs/content/docs/guides/tree-shaking.md b/apps/docs/content/docs/guides/tree-shaking.md index 78557213..ebab2a9f 100644 --- a/apps/docs/content/docs/guides/tree-shaking.md +++ b/apps/docs/content/docs/guides/tree-shaking.md @@ -32,8 +32,8 @@ Each method accepts the same config as its client as the first argument: Info methods use [`InfoClient`](../clients.md#info-endpoint) config: ```ts -import { HttpTransport } from "@bloxwap/hyperliquid"; -import { allMids } from "@bloxwap/hyperliquid/api/info"; +import { HttpTransport } from "@bloxwap/hyperliquid/transport/http"; +import { allMids } from "@bloxwap/hyperliquid/api/info/allMids"; const transport = new HttpTransport(); const result = await allMids({ transport }); @@ -42,8 +42,8 @@ const result = await allMids({ transport }); Exchange methods use [`ExchangeClient`](../clients.md#exchange-endpoint) config: ```ts -import { HttpTransport } from "@bloxwap/hyperliquid"; -import { order } from "@bloxwap/hyperliquid/api/exchange"; +import { HttpTransport } from "@bloxwap/hyperliquid/transport/http"; +import { order } from "@bloxwap/hyperliquid/api/exchange/order"; import { privateKeyToAccount } from "viem/accounts"; const transport = new HttpTransport(); @@ -80,9 +80,20 @@ const subscription = await allMids({ transport }, (data) => { Explorer methods use [`ExplorerClient`](../clients.md#explorer-endpoint) config: ```ts -import { HttpTransport } from "@bloxwap/hyperliquid"; +import { HttpTransport } from "@bloxwap/hyperliquid/transport/http"; import { blockDetails } from "@bloxwap/hyperliquid/api/explorer"; const transport = new HttpTransport(); const block = await blockDetails({ transport }, { height: 123 }); ``` + +## One-operation entry points + +Every public method is available at `@bloxwap/hyperliquid/api//`, including its parameter and response +types. The four families are `info`, `exchange`, `explorer`, and `subscription`. Existing API barrels and client paths +remain available. Per-operation imports reduce runtime module evaluation even when Node or Bun runs without a bundler; +API barrel imports rely on bundling/tree-shaking to remove sibling operations. + +Canonical builders are available at `@bloxwap/hyperliquid/actions/` (for example `buildOrder` from +`actions/order`). Import execution stages separately from `actions/execution` when you need only a few builders. +The `actions` barrel contains every builder. diff --git a/packages/hyperliquid/.dev/build/build.ts b/packages/hyperliquid/.dev/build/build.ts index db630a43..791cecc4 100644 --- a/packages/hyperliquid/.dev/build/build.ts +++ b/packages/hyperliquid/.dev/build/build.ts @@ -22,8 +22,9 @@ * @module */ -import { copyFile, readdir, rm, writeFile } from "node:fs/promises"; -import { join, resolve } from "node:path"; +import { copyFile, cp, mkdir, mkdtemp, readdir, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, relative as pathRelative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { build as esbuild } from "esbuild"; @@ -129,23 +130,157 @@ async function emitDeclarations(): Promise { * @throws If the bundler reports any error. */ async function bundleSources(root: RootManifest): Promise { - const result = await esbuild({ - entryPoints: Object.values(root.exports).map((target) => join(ROOT_DIR, target)), + // Per-operation entry points would force the client bundles into hundreds of tiny chunks. + // Bundle pure operation/schema code separately, but externalize every identity/state boundary + // to one shared core so mixed entry points keep instanceof, nonce, and action ownership intact. + const core = [ + "_base.ts", + "api/_errors.ts", + "signing/mod.ts", + "signing/_canonicalize.ts", + "transport/_base.ts", + "api/exchange/_methods/_base/_shell.ts", + "api/exchange/_methods/_base/_nonce.ts", + "api/exchange/_methods/_base/execute.ts", + "actions/_canonical.ts", + "actions/execution.ts", + "api/subscription/_methods/fastAssetCtxs.ts", + ]; + const coreFiles = new Map(); + const aggregators = new Map(); + for (const [index, name] of core.entries()) { + const file = join(ROOT_DIR, "src", name); + const group = ["_base.ts", "api/_errors.ts", "transport/_base.ts"].includes(name) + ? "runtime" + : name === "signing/mod.ts" + ? "signing" + : ["actions/_canonical.ts", "signing/_canonicalize.ts"].includes(name) + ? "canonical" + : name === "api/subscription/_methods/fastAssetCtxs.ts" + ? "subscriptions" + : "exchange"; + const names = Object.keys(await import(file)); + const aliases = names.map((value) => ({ value, alias: `m${index}_${value}` })); + const exports = aliases.map(({ value, alias }) => `${alias} as ${value}`).join(", "); + coreFiles.set(file, { group, exports }); + aggregators.set( + group, + (aggregators.get(group) ?? "") + + `export { ${aliases.map(({ value, alias }) => `${value} as ${alias}`).join(", ")} } from ${JSON.stringify(file)};\n`, + ); + } + const primary: string[] = []; + const narrow: string[] = []; + for (const target of new Set(Object.values(root.exports))) { + const file = join(ROOT_DIR, target); + const shared = coreFiles.get(file); + if (shared) { + const output = join(DIST_DIR, target.slice("./src/".length).replace(/\.ts$/, ".js")); + // Declarations already created the parent directories. + const specifier = pathRelative(dirname(output), join(DIST_DIR, "_core", `${shared.group}.js`)); + await writeFile( + output, + `export { ${shared.exports} } from ${JSON.stringify(specifier.startsWith(".") ? specifier : `./${specifier}`)};\n`, + ); + } else if ( + /\/api\/[^/]+\/_methods\/[^/]+\.ts$/.test(file) || + /\/actions\/(?!mod\.ts|orderBatcher\.ts)[^/]+\.ts$/.test(file) + ) { + narrow.push(file); + } else primary.push(file); + } + const common = { outbase: join(ROOT_DIR, "src"), - outdir: DIST_DIR, tsconfig: BUILD_TSCONFIG, bundle: true, - splitting: true, - format: "esm", - platform: "neutral", + format: "esm" as const, + platform: "neutral" as const, target: "es2024", - packages: "external", + packages: "external" as const, entryNames: "[dir]/[name]", - chunkNames: "_chunks/[name]-[hash]", - metafile: true, - logLevel: "warning", - }); - return Object.keys(result.metafile.outputs).length; + // Keep readable identifiers while avoiding comment/whitespace parsing at startup. + minifyWhitespace: true, + metafile: true as const, + logLevel: "warning" as const, + }; + const externalCore: import("esbuild").Plugin = { + name: "shared-sdk-core", + setup(builder): void { + builder.onResolve({ filter: /^\./ }, (args) => { + const resolved = resolve(args.resolveDir, args.path); + return coreFiles.has(resolved) + ? { path: resolved, namespace: "sdk-core-proxy", sideEffects: false } + : undefined; + }); + // Internal proxies give esbuild explicit names for external core exports, so + // overlapping wildcard exports remain unambiguous and proxies add no runtime files. + builder.onLoad({ filter: /.*/, namespace: "sdk-core-proxy" }, (args) => { + const shared = coreFiles.get(args.path)!; + return { + contents: `export { ${shared.exports} } from ${JSON.stringify(join(DIST_DIR, "_core", `${shared.group}.js`))};`, + loader: "js", + }; + }); + builder.onResolve({ filter: /.*/, namespace: "sdk-core-proxy" }, (args) => ({ + path: args.path, + external: true, + sideEffects: false, + })); + }, + }; + const aggregateCore: import("esbuild").Plugin = { + name: "aggregate-sdk-core", + setup(builder): void { + builder.onResolve({ filter: /^sdk-core:/ }, (args) => ({ + path: args.path.slice("sdk-core:".length), + namespace: "sdk-core", + })); + builder.onLoad({ filter: /.*/, namespace: "sdk-core" }, (args) => ({ + contents: aggregators.get(args.path)!, + loader: "js", + resolveDir: ROOT_DIR, + })); + }, + }; + const outputs = [ + await esbuild({ + ...common, + entryPoints: [ + ...primary.map((file) => ({ in: file, out: pathRelative(join(ROOT_DIR, "src"), file).replace(/\.ts$/, "") })), + ...[...aggregators.keys()].map((group) => ({ in: `sdk-core:${group}`, out: `_core/${group}` })), + ], + plugins: [aggregateCore], + outdir: DIST_DIR, + splitting: true, + chunkNames: "_chunks/[name]-[hash]", + }), + await esbuild({ ...common, entryPoints: narrow, outdir: DIST_DIR, splitting: false, plugins: [externalCore] }), + ]; + let count = core.filter((name) => Object.values(root.exports).includes(`./src/${name}`)).length; + for (const output of outputs) + for (const file of Object.keys(output.metafile.outputs)) { + const absolute = resolve(ROOT_DIR, file); + const code = await Bun.file(absolute).text(); + const portable = code.replace(/(?:\bfrom\s*|\bimport\s*)"([^"\n]+)"/g, (match, specifier: string) => { + if (!specifier.startsWith(`${DIST_DIR}/_core/`)) return match; + const relative = pathRelative(dirname(absolute), specifier); + return match.replace( + JSON.stringify(specifier), + JSON.stringify(relative.startsWith(".") ? relative : `./${relative}`), + ); + }); + // The package promises sideEffects:false. esbuild's splitting emits bare chunk + // imports for evaluation ordering, even when that entry uses none of their bindings. + // Bundled consumers drop these already; unbundled Node/Bun consumers need the same + // behavior to prevent an Info-only import from evaluating exchange/signing chunks. + const isolated = portable.replace(/\bimport\s*"(\.{1,2}\/[^"\n]+)"\s*;/g, (match, specifier: string) => { + return resolve(dirname(absolute), specifier).startsWith(join(DIST_DIR, "_chunks")) ? "" : match; + }); + if (isolated.includes(DIST_DIR)) throw new Error(`Non-portable SDK path in ${file}`); + if (isolated !== code) await writeFile(absolute, isolated); + count++; + } + return count; } /** @@ -159,6 +294,24 @@ async function bundleSources(root: RootManifest): Promise { * @throws If Node cannot load an entry, or an entry's export names differ from its source's. */ async function verifyBundle(root: RootManifest): Promise { + for (const entry of ["api/info/client.js", "api/info/_methods/allMids.js", "transport/http/mod.js"]) { + const pending = [join(DIST_DIR, entry)]; + const seen = new Set(); + while (pending.length) { + const file = pending.pop()!; + if (seen.has(file)) continue; + seen.add(file); + const code = await Bun.file(file).text(); + if ( + /\b(?:function (?:signL1Action|createL1ActionHash)|class (?:ExchangeClient|SubscriptionClient))\b/.test(code) + ) { + throw new Error(`Read-only ${entry} evaluates exchange/subscription/signing code through ${file}`); + } + for (const match of code.matchAll(/\b(?:from\s*|import\s*)"(\.{1,2}\/[^"\n]+)"/g)) { + pending.push(resolve(dirname(file), match[1])); + } + } + } const expected: Record = {}; for (const target of Object.values(root.exports)) { const source = (await import(join(ROOT_DIR, target))) as Record; @@ -235,9 +388,24 @@ async function writeDistManifest(root: RootManifest): Promise { if (root[key] !== undefined) manifest[key] = root[key]; } // ESM-only package: consumers resolve through `exports` alone, so no `main`/`module`/`types` fallbacks are emitted. - manifest.exports = Object.fromEntries( - Object.entries(root.exports).map(([subpath, target]) => [subpath, toEmittedConditions(target)]), - ); + // Hundreds of repetitive conditions enlarge package.json and slow Node's package-scope + // lookup on every cold import. Source exports stay explicit and checked; published operation + // families use the same file layout through compact patterns. Private underscore paths stay closed. + const exports: Record = {}; + for (const [subpath, target] of Object.entries(root.exports)) { + if (target.includes("/_methods/") || subpath.startsWith("./actions/")) continue; + exports[subpath] = toEmittedConditions(target); + } + for (const family of ["info", "exchange", "explorer", "subscription"]) { + exports[`./api/${family}/*`] = { + types: `./api/${family}/_methods/*.d.ts`, + default: `./api/${family}/_methods/*.js`, + }; + exports[`./api/${family}/_*`] = null; + } + exports["./actions/*"] = { types: "./actions/*.d.ts", default: "./actions/*.js" }; + exports["./actions/_*"] = null; + manifest.exports = exports; await writeFile(join(DIST_DIR, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`); } @@ -247,6 +415,54 @@ async function copyDocs(): Promise { await Promise.all(COPIED_FILES.map((name) => copyFile(join(ROOT_DIR, name), join(DIST_DIR, name)))); } +/** Check public runtime resolution, shared state, and type inference after relocating the package. */ +async function verifyConsumer(root: RootManifest): Promise { + const consumer = await mkdtemp(join(tmpdir(), "hl-published-consumer-")); + try { + const modules = join(consumer, "node_modules"); + await mkdir(join(modules, "@bloxwap"), { recursive: true }); + await cp(DIST_DIR, join(modules, root.name), { recursive: true }); + for (const name of Object.keys(root.dependencies as Record)) { + const target = join(modules, name); + await mkdir(dirname(target), { recursive: true }); + await symlink(join(ROOT_DIR, "node_modules", name), target, "dir"); + } + await writeFile(join(consumer, "package.json"), '{"type":"module"}\n'); + const entries: Record = {}; + for (const [subpath, target] of Object.entries(root.exports)) { + const specifier = subpath === "." ? root.name : `${root.name}${subpath.slice(1)}`; + entries[specifier] = Object.keys(await import(join(ROOT_DIR, target))).sort(); + } + await writeFile(join(consumer, "entries.json"), JSON.stringify(entries)); + for (const name of ["consumer.ts", "consumer.mjs"]) { + await copyFile(join(ROOT_DIR, ".dev/build", name), join(consumer, name)); + } + const commands = [ + ["node", "consumer.mjs"], + [process.execPath, "consumer.mjs"], + [ + process.execPath, + join(ROOT_DIR, "node_modules/typescript/bin/tsc"), + "consumer.ts", + "--noEmit", + "--strict", + "--skipLibCheck", + "--module", + "nodenext", + "--target", + "es2024", + ], + ]; + for (const command of commands) { + const child = Bun.spawn(command, { cwd: consumer, stdio: ["inherit", "inherit", "inherit"] }); + const code = await child.exited; + if (code !== 0) throw new Error(`Published consumer failed: ${command[0]} (exit ${code})`); + } + } finally { + await rm(consumer, { recursive: true, force: true }); + } +} + // --- Entry point ------------------------------------------------------------- /** @@ -265,6 +481,7 @@ export async function build(): Promise { const rewritten = await rewriteDeclarationExtensions(DIST_DIR); await writeDistManifest(root); await copyDocs(); + await verifyConsumer(root); console.log( `Built ${root.name}@${root.version} into dist/ (${bundled} JavaScript files, ${rewritten} declaration files rewritten).`, diff --git a/packages/hyperliquid/.dev/build/consumer.mjs b/packages/hyperliquid/.dev/build/consumer.mjs new file mode 100644 index 00000000..a75c7352 --- /dev/null +++ b/packages/hyperliquid/.dev/build/consumer.mjs @@ -0,0 +1,61 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import * as root from "@bloxwap/hyperliquid"; +import { ExchangeClient } from "@bloxwap/hyperliquid/api/exchange/client"; +import { ApiRequestError } from "@bloxwap/hyperliquid/api/exchange"; +import { HttpTransport } from "@bloxwap/hyperliquid/transport/http"; +import { allMids } from "@bloxwap/hyperliquid/api/info/allMids"; +import { buildOrder } from "@bloxwap/hyperliquid/actions/order"; +import { order } from "@bloxwap/hyperliquid/api/exchange/order"; +import { signAction, submitAction } from "@bloxwap/hyperliquid/actions/execution"; +import { fastAssetCtxs as barrelFastAssetCtxs } from "@bloxwap/hyperliquid/api/subscription"; +import { fastAssetCtxs as individualFastAssetCtxs } from "@bloxwap/hyperliquid/api/subscription/fastAssetCtxs"; + +assert.equal(root.ExchangeClient, ExchangeClient); +assert.equal(root.HttpTransport, HttpTransport); +assert.equal(root.ApiRequestError, ApiRequestError); +assert.equal(barrelFastAssetCtxs, individualFastAssetCtxs); +const expectedEntries = JSON.parse(readFileSync(new URL("./entries.json", import.meta.url), "utf8")); +for (const [specifier, expected] of Object.entries(expectedEntries)) { + assert.deepEqual(Object.keys(await import(specifier)).sort(), expected, specifier); +} +for (const path of ["actions/_canonical", "api/exchange/_base/execute", "api/info/_base/_config"]) { + await assert.rejects(import(`@bloxwap/hyperliquid/${path}`)); +} +const nonces = []; +let signCount = 0; +let failure = false; +const transport = { + isTestnet: true, + async request(endpoint, body) { + if (endpoint === "info") return { BTC: "1" }; + nonces.push(body.nonce); + if (failure) return { status: "err", response: "test failure" }; + return { status: "ok", response: { type: "order", data: { statuses: [{ resting: { oid: 1 } }] } } }; + }, +}; +const config = { + transport, + wallet: { + address: `0x${"42".repeat(20)}`, + async signTypedData() { + signCount++; + return `0x${"11".repeat(64)}1b`; + }, + }, +}; +const client = new root.ExchangeClient(config); +const params = { orders: [{ a: 0, b: true, p: "1", s: "1", r: false, t: { limit: { tif: "Gtc" } } }] }; +const action = buildOrder(params); +assert(Object.isFrozen(action.payload.orders)); +assert.deepEqual(await allMids({ transport }), { BTC: "1" }); +const signed = await client.sign(action); +assert.equal(nonces.length, 0); +await submitAction(config, signed); +assert.equal(signCount, 1); +await client.submit(await signAction(config, action)); +await Promise.all([client.execute(action), order(config, params)]); +assert.equal(new Set(nonces).size, nonces.length); +failure = true; +await assert.rejects(client.execute(action), ApiRequestError); +console.log("Published consumer imports, identities, nonce sharing, signing ownership, and error types passed."); diff --git a/packages/hyperliquid/.dev/build/consumer.ts b/packages/hyperliquid/.dev/build/consumer.ts new file mode 100644 index 00000000..d9393bf5 --- /dev/null +++ b/packages/hyperliquid/.dev/build/consumer.ts @@ -0,0 +1,30 @@ +/** Type-check against the relocated published package, rather than source aliases. */ +import { ExchangeClient, HttpTransport } from "@bloxwap/hyperliquid"; +import { allMids } from "@bloxwap/hyperliquid/api/info/allMids"; +import { buildOrder } from "@bloxwap/hyperliquid/actions/order"; +import { executeAction, signAction, submitAction } from "@bloxwap/hyperliquid/actions/execution"; +import type { OrderSuccessResponse } from "@bloxwap/hyperliquid/api/exchange/order"; +import type { ExchangeConfig } from "@bloxwap/hyperliquid/api/exchange"; +// @ts-expect-error Private declaration paths remain closed by the published export map. +import type { ActionMetadata } from "@bloxwap/hyperliquid/actions/_canonical"; +void ({} as ActionMetadata); + +declare const config: ExchangeConfig; +const transport = new HttpTransport(); +const mids: Record = await allMids({ transport }); +void mids; +const input = { a: 0, b: true, p: "1", s: "1", r: false, t: { limit: { tif: "Gtc" as const } } }; +const action = buildOrder({ orders: [input] }); +const client = new ExchangeClient(config); +const signed = await client.sign(action); +const submitted: OrderSuccessResponse = await client.submit(signed); +const executed: OrderSuccessResponse = await client.execute(action); +const staged: OrderSuccessResponse = await submitAction(config, await signAction(config, action)); +const direct: OrderSuccessResponse = await executeAction(config, action); +void [submitted, executed, staged, direct]; +// @ts-expect-error The published type exposes an immutable signature. +signed.signature.v = 28; +// @ts-expect-error Signed action fields are immutable as well. +signed.action.type = "cancel"; +// @ts-expect-error Operation-specific inputs retain their literal union types. +buildOrder({ orders: [{ ...input, t: { limit: { tif: "invalid" } } }] }); diff --git a/packages/hyperliquid/.dev/export_sync_check.ts b/packages/hyperliquid/.dev/export_sync_check.ts index 9e4656cc..5de88fa9 100644 --- a/packages/hyperliquid/.dev/export_sync_check.ts +++ b/packages/hyperliquid/.dev/export_sync_check.ts @@ -387,6 +387,19 @@ async function main(): Promise { for (const endpoint of API_ENDPOINTS) { // Get methods from _methods directory const methodsFromDir = await getMethodsFromDir(endpoint.methodsDir); + const manifest = await Bun.file(path.join(process.cwd(), "package.json")).json(); + for (const methodName of methodsFromDir) { + const expected = `./${endpoint.methodsDir}/${methodName}.ts`; + if (manifest.exports?.[`./api/${endpoint.name}/${methodName}`] !== expected) { + allErrors.push({ + scope: "exports", + subject: methodName, + errorType: "missing operation entry point", + details: `Expected ./api/${endpoint.name}/${methodName} to export ${expected}`, + filePath: "package.json", + }); + } + } // Parse mod.ts exports const modExports = await parseModExports(endpoint.modPath); diff --git a/packages/hyperliquid/.dev/import_graph_check.ts b/packages/hyperliquid/.dev/import_graph_check.ts index 5a97a29c..cfe09462 100644 --- a/packages/hyperliquid/.dev/import_graph_check.ts +++ b/packages/hyperliquid/.dev/import_graph_check.ts @@ -22,7 +22,7 @@ */ import { readFileSync } from "node:fs"; -import { dirname, relative, resolve } from "node:path"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import process from "node:process"; import ts from "typescript"; @@ -62,6 +62,21 @@ const BUDGETS: readonly { entry: string; limit: number; why: string }[] = [ limit: 110, why: "The narrow read-only entry point. It legitimately pulls the Info methods it wraps, but must not also pull the exchange, subscription or signing graphs.", }, + { + entry: "src/api/info/_methods/allMids.ts", + limit: 12, + why: "A single Info operation must not load sibling operations or signing.", + }, + { + entry: "src/api/explorer/_methods/explorerBlock.ts", + limit: 12, + why: "A single Explorer operation must remain independent of the other API families.", + }, + { + entry: "src/actions/order.ts", + limit: 35, + why: "An individual builder must not load the ExchangeClient or sibling action schemas.", + }, ]; // ============================================================================= diff --git a/packages/hyperliquid/.dev/jsdoc_sync_check.ts b/packages/hyperliquid/.dev/jsdoc_sync_check.ts index 05229dcf..bab8080a 100644 --- a/packages/hyperliquid/.dev/jsdoc_sync_check.ts +++ b/packages/hyperliquid/.dev/jsdoc_sync_check.ts @@ -472,7 +472,13 @@ function main(): void { // Compare each method with its corresponding function for (const [methodName, methodJSDoc] of methodsJSDoc) { - const funcFilePath = path.join(methodsDir, `${methodName}.ts`); + // Explicit action stages live in a shared module, rather than endpoint method files. + const stages: Record = + endpoint.name === "exchange" ? { sign: "signAction", submit: "submitAction", execute: "executeAction" } : {}; + const functionName = stages[methodName] ?? methodName; + const funcFilePath = stages[methodName] + ? path.join(projectRoot, "src/actions/execution.ts") + : path.join(methodsDir, `${methodName}.ts`); // An undocumented method cannot be "in sync" with anything, so report it without reading the function side. if (methodJSDoc === null) { @@ -499,7 +505,7 @@ function main(): void { // Parse function JSDoc from _methods/*.ts const funcJSDocMap = parseFunctionJSDoc(funcFilePath); - const funcJSDoc = funcJSDocMap.get(methodName); + const funcJSDoc = funcJSDocMap.get(functionName); // Check if function exists and has JSDoc if (!funcJSDoc) { diff --git a/packages/hyperliquid/.dev/perf/optimization_search.ts b/packages/hyperliquid/.dev/perf/optimization_search.ts new file mode 100644 index 00000000..93f1650e --- /dev/null +++ b/packages/hyperliquid/.dev/perf/optimization_search.ts @@ -0,0 +1,219 @@ +/** + * Three offline optimization experiments, kept outside the fingerprinted regression suite. + * Run with Bun or Node 24+: .dev/perf/optimization_search.ts --out /tmp/search.json + * Rotates paired variants, checks identical bytes, varies nonces, and measures cold setup separately. + * @module + */ +import { strict as assert } from "node:assert"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { cpus, tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { buildOrder } from "../../src/api/exchange/_methods/order.ts"; +import { MsgpackWriter, type L1Value } from "../../src/signing/_msgpack.ts"; + +const sampleCount = 11; +const results: { scenario: string; medianUs: number; p90Us: number; roundsUs: number[] }[] = []; +let sink = 0; +const baseNonce = 1_700_000_000_000; +const input = { a: 0, b: true, p: "30000", s: "0.1", r: false, t: { limit: { tif: "Gtc" as const } } }; +const wasm = await import("hash-wasm").then( + (m) => m.createKeccak(256), + () => undefined, +); + +function paired(group: string, variants: Record number>, iterations: number): void { + const entries = Object.entries(variants); + const rounds = entries.map(() => [] as number[]); + for (let round = -3; round < sampleCount; round++) { + for (let index = 0; index < entries.length; index++) { + const selected = (index + round + entries.length * 3) % entries.length; + const run = entries[selected][1]; + const start = performance.now(); + for (let i = 0; i < iterations; i++) sink ^= run(i); + if (round >= 0) rounds[selected].push(((performance.now() - start) * 1000) / iterations); + } + } + entries.forEach(([name], i) => { + const sorted = [...rounds[i]].sort((a, b) => a - b); + const result = { + scenario: `${group}/${name}`, + medianUs: sorted[Math.floor(sorted.length / 2)], + p90Us: sorted[Math.ceil(sorted.length * 0.9) - 1], + roundsUs: rounds[i], + }; + results.push(result); + console.log(`${result.scenario}: ${result.medianUs.toFixed(3)} us`); + }); +} + +const directory = await mkdtemp(join(tmpdir(), "hyperliquid-ocean-")); +try { + const source = await readFile(new URL("../../src/signing/_msgpack.ts", import.meta.url), "utf8"); + const singleAscii = source.replace( + " const length = value.length;\n if (length >= 4294967296)", + ` const length = value.length; + if (length === 1 && value.charCodeAt(0) <= 0x7f) { + this.ensure(2); + this.buffer[this.offset++] = 0xa1; + this.buffer[this.offset++] = value.charCodeAt(0); + return; + } + if (length >= 4294967296)`, + ); + assert.notEqual(singleAscii, source); + const keyCache = source.replaceAll("this.string(key);", "this.mapKey(key);").replace( + " private string(value: string): void {", + ` private readonly keys = new Map(); + private mapKey(value: string): void { + let bytes = this.keys.get(value); + if (bytes === undefined) { + const start = this.offset; + this.string(value); + if (this.keys.size < 64) this.keys.set(value, this.buffer.slice(start, this.offset)); + return; + } + this.raw(bytes); + } + private string(value: string): void {`, + ); + const writers: Record = { current: new MsgpackWriter() }; + for (const [name, variant] of Object.entries({ single_ascii: singleAscii, cached_keys: keyCache })) { + const path = join(directory, `${name}.ts`); + await writeFile(path, variant); + const { MsgpackWriter: Writer } = await import(pathToFileURL(path).href); + writers[name] = new Writer(); + } + + for (const count of [1, 10, 100, 1000]) { + const action = buildOrder({ orders: Array.from({ length: count }, (_, i) => ({ ...input, a: i % 100 })) }); + const mutable = structuredClone(action.payload); + const writer = new MsgpackWriter(); + writer.valueL1(action.payload as L1Value); + const prefix = writer.view().slice(); + const nobleState = keccak_256.create().update(prefix); + let wasmState: Uint8Array | undefined; + if (wasm) { + wasm.init(); + wasm.update(prefix); + wasmState = wasm.save(); + } + const fullWriter = new MsgpackWriter(); + const tailWriter = new MsgpackWriter(); + const full = (i: number): Uint8Array => { + fullWriter.reset(); + fullWriter.raw(prefix); + fullWriter.uint64(baseNonce + i); + fullWriter.byte(0); + return fullWriter.view(); + }; + const tail = (i: number): Uint8Array => { + tailWriter.reset(); + tailWriter.uint64(baseNonce + i); + tailWriter.byte(0); + return tailWriter.view(); + }; + const resumeNoble = (i: number): Uint8Array => nobleState.clone().update(tail(i)).digest(); + const resumeWasm = (i: number): Uint8Array => { + wasm!.load(wasmState!); + wasm!.update(tail(i)); + return wasm!.digest("binary"); + }; + for (const i of [0, 1, 255, 65536]) { + const expected = keccak_256(full(i)); + assert.deepEqual(resumeNoble(i), expected); + if (wasm) assert.deepEqual(resumeWasm(i), expected); + } + const iterations = Math.max(100, Math.floor(30_000 / count)); + paired( + `checkpoint_${count}`, + { + noble_full: (i) => keccak_256(full(i))[0], + noble_resume: (i) => resumeNoble(i)[0], + noble_cold: (i) => keccak_256.create().update(prefix).clone().update(tail(i)).digest()[0], + ...(wasm + ? { + wasm_full: (i: number) => { + wasm.init(); + wasm.update(full(i)); + return wasm.digest("binary")[0]; + }, + wasm_resume: (i: number) => resumeWasm(i)[0], + wasm_cold: (i: number) => { + wasm.init(); + wasm.update(prefix); + const state = wasm.save(); + wasm.load(state); + wasm.update(tail(i)); + return wasm.digest("binary")[0]; + }, + } + : {}), + }, + iterations, + ); + + writer.reset(); + writer.valueL1(mutable as L1Value); + const reference = writer.view().slice(); + for (const candidate of Object.values(writers)) { + candidate.reset(); + candidate.valueL1(mutable as L1Value); + assert.deepEqual(candidate.view(), reference); + } + paired( + `atoms_${count}`, + Object.fromEntries( + Object.entries(writers).map(([name, candidate]) => [ + name, + () => { + candidate.reset(); + candidate.valueL1(mutable as L1Value); + return candidate.view()[0]; + }, + ]), + ), + iterations, + ); + + const signature = { r: `0x${"11".repeat(32)}`, s: `0x${"22".repeat(32)}`, v: 27 }; + const signatureJSON = JSON.stringify(signature); + const actionJSON = JSON.stringify(action.payload); + const reusedJSON = (i: number): string => + `{"action":${actionJSON},"signature":${signatureJSON},"nonce":${baseNonce + i}}`; + const request = (i: number) => ({ action: action.payload, signature, nonce: baseNonce + i }); + for (const i of [0, 1, 255]) assert.equal(reusedJSON(i), JSON.stringify(request(i))); + paired( + `wire_${count}`, + { + full_json: (i) => JSON.stringify(request(i)).length, + action_fragment: (i) => reusedJSON(i).length, + // Encoding forces the concatenation to materialize, as a real socket/fetch must. + full_json_utf8: (i) => new TextEncoder().encode(JSON.stringify(request(i))).length, + action_fragment_utf8: (i) => new TextEncoder().encode(reusedJSON(i)).length, + }, + iterations, + ); + } +} finally { + await rm(directory, { recursive: true, force: true }); +} + +const outIndex = process.argv.indexOf("--out"); +if (outIndex >= 0) + await writeFile( + process.argv[outIndex + 1], + `${JSON.stringify( + { + runtime: process.versions, + cpu: cpus()[0]?.model, + date: new Date().toISOString(), + sampleCount, + sink, + results, + }, + null, + 2, + )}\n`, + ); diff --git a/packages/hyperliquid/.dev/perf/optimization_verify.ts b/packages/hyperliquid/.dev/perf/optimization_verify.ts new file mode 100644 index 00000000..489c276f --- /dev/null +++ b/packages/hyperliquid/.dev/perf/optimization_verify.ts @@ -0,0 +1,254 @@ +/** + * Pair the actual SDK before/after implementations, including signing and both wire transports. + * .dev/perf/optimization_verify.ts --compare-dir --out /tmp/verify.json + * The snapshot needs src/, package.json, and access to the same node_modules. No network calls. + * --filter staged limits the run to separate sign/submit; other scenario substrings also work. + * @module + */ +import { strict as assert } from "node:assert"; +import { writeFile } from "node:fs/promises"; +import { cpus } from "node:os"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +const compareIndex = process.argv.indexOf("--compare-dir"); +assert(compareIndex >= 0, "Pass --compare-dir pointing to the original SDK package snapshot"); +const compareDir = resolve(process.argv[compareIndex + 1]); +const filterIndex = process.argv.indexOf("--filter"); +const filter = filterIndex >= 0 ? process.argv[filterIndex + 1] : undefined; +const matches = (scenario: string): boolean => filter === undefined || scenario.includes(filter); +async function sdk(url: URL) { + const load = (file: string) => import(new URL(`src/${file}`, url).href); + const [order, execution, l1, keccak, http, ws, quota, wallet] = await Promise.all([ + load("api/exchange/_methods/order.ts"), + load("actions/execution.ts"), + load("signing/_l1.ts"), + load("signing/_keccak.ts"), + load("transport/http/mod.ts"), + load("transport/websocket/mod.ts"), + load("transport/websocket/_quota.ts"), + load("signing/_fastWallet.ts"), + ]); + return { ...order, ...execution, ...l1, ...keccak, ...http, ...ws, ...quota, ...wallet }; +} +const variants = { + before: await sdk(pathToFileURL(`${compareDir}/`)), + after: await sdk(new URL("../../", import.meta.url)), +}; +const sampleCount = 11; +const results: { scenario: string; variant: string; medianUs: number; p90Us: number; roundsUs: number[] }[] = []; +let sink = 0; +const nonce = 1_700_000_000_000; +const input = { a: 0, b: true, p: "30000", s: "0.1", r: false, t: { limit: { tif: "Gtc" as const } } }; +const encoder = new TextEncoder(); +const ok = { status: "ok", response: { type: "default" } }; +const signature = `0x${"11".repeat(64)}1b` as const; + +function record(group: string, names: string[], rounds: number[][]): void { + names.forEach((variant, i) => { + const sorted = [...rounds[i]].sort((a, b) => a - b); + const result = { + scenario: group, + variant, + medianUs: sorted[Math.floor(sorted.length / 2)], + p90Us: sorted[Math.ceil(sorted.length * 0.9) - 1], + roundsUs: rounds[i], + }; + results.push(result); + console.log(`${group}/${variant}: ${result.medianUs.toFixed(3)} us`); + }); +} + +function paired(group: string, cases: Record number>, iterations: number): void { + const entries = Object.entries(cases); + const rounds = entries.map(() => [] as number[]); + for (let round = -3; round < sampleCount; round++) + for (let index = 0; index < entries.length; index++) { + const selected = (index + round + entries.length * 3) % entries.length; + const start = performance.now(); + for (let i = 0; i < iterations; i++) sink ^= entries[selected][1](i); + if (round >= 0) rounds[selected].push(((performance.now() - start) * 1000) / iterations); + } + record( + group, + entries.map(([name]) => name), + rounds, + ); +} + +async function pairedAsync( + group: string, + cases: Record Promise>, + iterations: number, +): Promise { + const entries = Object.entries(cases); + const rounds = entries.map(() => [] as number[]); + for (let round = -3; round < sampleCount; round++) + for (let index = 0; index < entries.length; index++) { + const selected = (index + round + entries.length * 3) % entries.length; + const start = performance.now(); + for (let i = 0; i < iterations; i++) await entries[selected][1](i); + if (round >= 0) rounds[selected].push(((performance.now() - start) * 1000) / iterations); + } + record( + group, + entries.map(([name]) => name), + rounds, + ); +} + +class Socket extends EventTarget { + readyState = 0; + bufferedAmount = 0; + binaryType = "blob"; + protocol = ""; + extensions = ""; + lastFrame = ""; + open(): void { + this.readyState = 1; + this.dispatchEvent(new Event("open")); + } + send(data: string): void { + this.lastFrame = data; + sink ^= encoder.encode(data).length; + const id = Number(/^\{"method":"post","id":(\d+),/.exec(data)?.[1]); + assert(Number.isSafeInteger(id)); + queueMicrotask(() => + this.dispatchEvent( + new MessageEvent("message", { + data: JSON.stringify({ channel: "post", data: { id, response: { type: "action", payload: ok } } }), + }), + ), + ); + } + close(): void { + this.readyState = 3; + this.dispatchEvent(Object.assign(new Event("close"), { code: 1000, reason: "", wasClean: true })); + } +} + +for (const provider of ["wasm", "noble"]) { + for (const impl of Object.values(variants)) { + impl._setKeccakLoaderForTests(provider === "noble" ? async () => undefined : undefined); + await impl.preloadWasmKeccak(); + } + for (const count of [1, 4, 8, 10, 100, 1000]) { + const scenario = `hash_${provider}_${count}`; + if (!matches(scenario)) continue; + const cases: Record number> = {}; + let expected: Uint8Array | undefined; + for (const [name, impl] of Object.entries(variants)) { + const built = impl.buildOrder({ orders: Array.from({ length: count }, () => input) }); + const mutable = structuredClone(built.payload); + const actual = impl.createL1ActionHashBytes({ action: built.payload, nonce }); + if (expected) assert.deepEqual(actual, expected); + expected = actual; + for (const [ownership, action] of Object.entries({ owned: built.payload, mutable })) { + cases[`${name}_${ownership}`] = (i) => impl.createL1ActionHashBytes({ action, nonce: nonce + i })[0]; + } + } + paired(scenario, cases, Math.max(50, Math.floor(5000 / count))); + } +} +for (const impl of Object.values(variants)) { + impl._setKeccakLoaderForTests(undefined); + await impl.preloadWasmKeccak(); +} + +const originalFetch = globalThis.fetch; +const originalWebSocket = globalThis.WebSocket; + +try { + for (const path of ["execute", "staged"]) + for (const curve of ["stub", "wasm"]) + for (const count of [1, 8, 100, 1000]) + for (const wire of ["memory", "http", "ws"]) { + const scenario = `${path}_${curve}_${wire}_${count}`; + if (!matches(scenario)) continue; + const cases: Record Promise> = {}; + const close: (() => Promise)[] = []; + for (const [name, impl] of Object.entries(variants)) { + const action = impl.buildOrder({ orders: Array.from({ length: count }, () => input) }); + let nextNonce = nonce; + const wallet = + curve === "wasm" + ? await impl.createFastLocalWallet(`0x${"11".repeat(32)}`) + : { + address: "0x1111111111111111111111111111111111111111", + sign: async () => signature, + signTypedData: async () => signature, + }; + let transport: { + isTestnet: boolean; + request: (...args: unknown[]) => Promise; + close?: () => Promise; + ready?: () => Promise; + }; + if (wire === "memory") + transport = { + isTestnet: true, + async request() { + return ok; + }, + }; + else if (wire === "http") { + globalThis.fetch = (async (_url: unknown, init: RequestInit) => { + sink ^= encoder.encode(init.body as string).length; + return new Response(JSON.stringify(ok), { headers: { "Content-Type": "application/json" } }); + }) as typeof fetch; + transport = new impl.HttpTransport({ isTestnet: true, timeout: null }); + } else { + let socket!: Socket; + globalThis.WebSocket = class extends Socket { + constructor() { + super(); + socket = this; + queueMicrotask(() => this.open()); + } + } as unknown as typeof WebSocket; + transport = new impl.WebSocketTransport({ + isTestnet: true, + timeout: null, + keepAlive: { interval: 60_000 }, + quota: new impl.WebSocketQuota(), + }); + await transport.ready!(); + assert(socket.readyState === 1); + close.push(() => transport.close!()); + } + const config = { wallet, transport, nonceManager: () => nextNonce++ }; + cases[name] = async () => { + if (path === "execute") await impl.executeAction(config, action); + else await impl.submitAction(config, await impl.signAction(config, action)); + }; + } + try { + await pairedAsync(scenario, cases, count === 1000 ? 50 : 150); + } finally { + for (const dispose of close) await dispose(); + } + } +} finally { + globalThis.fetch = originalFetch; + globalThis.WebSocket = originalWebSocket; +} + +const outIndex = process.argv.indexOf("--out"); +if (outIndex >= 0) + await writeFile( + process.argv[outIndex + 1], + `${JSON.stringify( + { + runtime: process.versions, + cpu: cpus()[0]?.model, + date: new Date().toISOString(), + compareDir, + filter, + sampleCount, + sink, + results, + }, + null, + 2, + )}\n`, + ); diff --git a/packages/hyperliquid/.dev/perf/results/optimization_summary.json b/packages/hyperliquid/.dev/perf/results/optimization_summary.json new file mode 100644 index 00000000..4588d62a --- /dev/null +++ b/packages/hyperliquid/.dev/perf/results/optimization_summary.json @@ -0,0 +1,2709 @@ +{ + "baseline": "Initial working tree at fcc3aca8d561a10167c3433f618f0b6b4740a270, including preexisting uncommitted optimizations; production comparisons use a snapshot restored to the files read before these edits.", + "units": "Microseconds per complete action/hash, never per order; these are local CPU measurements.", + "decisions": [ + { + "idea": "Nonce-only Keccak checkpoints", + "decision": "implemented", + "policy": "Only reused SDK-owned immutable prefixes >=272 bytes; lazy noble clone or independently validated WASM save/load; provider-instance changes rebuild state. Nonce/vault/expiry and signatures stay fresh." + }, + { + "idea": "Immutable action JSON templates", + "decision": "implemented", + "policy": "Only SDK-owned bulk arrays >=8 entries in shell-owned envelopes; descriptor and identity checks preserve fallback behavior. WeakMap ownership. Signature/metadata serialized fresh. Synthetic fragment candidates preencode a fixed signature; use productionExecutionComparisons for actual SDK claims. Separate sign/submit preserves owned payload identity, freezes fresh signature metadata, and registers the signed envelope." + }, + { + "idea": "MessagePack atoms", + "decision": "rejected", + "policy": "Single-character ASCII and a bounded encoded-key dictionary both slowed the existing writer on both runtimes; no production MessagePack changes." + } + ], + "validation": { + "offlineTests": { + "runtime": "Bun 1.4.0-canary.1", + "passed": 2030, + "skipped": 242, + "failed": 0, + "assertions": 33395 + }, + "typescript5": "passed", + "typescript7": "passed with installed Bun 1.4.0 for frozen-lockfile check", + "jsdoc": "passed", + "exports": "passed", + "importGraph": "all 7 budgets passed", + "formatAndLint": "passed for changed TypeScript files", + "build": "passed; relocated published consumers validated on Bun and Node", + "originalPerformanceSuite": { + "compared": 57, + "faster": 1, + "regressed": 0, + "unchanged": 56, + "suiteFingerprint": "e8136f5304a22d9f", + "beforeReport": "/tmp/hyperliquid-perf-before.json", + "afterReport": "/tmp/hyperliquid-perf-after.json" + }, + "finalTargetedConformance": { + "passed": 40, + "failed": 0, + "assertions": 1427 + } + }, + "sources": { + "src/signing/_keccak.ts": "078eb357ebe3bc511a137d094dc4e9abb6985b9730a2b2d47ac1f849991c5b95", + "src/signing/_l1.ts": "89af7fca9ef8e3783964852691a213d359b4935a5acce545640274ccbface0d5", + "src/signing/_l1Cache.ts": "7bc92c87189805db32a2c2be9c2220878dded8ebe05927fce3f707086e276a03", + "src/transport/_wire.ts": "0b4d48e0c2ba49e260a6a8454be988c769527594453c5ead5aa10e653c6925f1", + "src/transport/http/mod.ts": "2fbc5ca13e446335a5faa8803894dc0dbc4993509f27d1a20515818519ab8f79", + "src/transport/websocket/mod.ts": "e4de10c91d8c257a04a08ea22919a92d9ca5316d6ebb96f651ebfc5a7e6665d4", + "src/transport/websocket/_dispatcher.ts": "2da747f422f3938f08c0b64abe7b55ea12270de520d21af152cc6c75229100be", + "src/actions/_canonical.ts": "b511a9c5e368ec5a84c5c3f86d3b18be96be912f66e77aff2c6c60d6ca662a1f", + "src/api/exchange/_methods/_base/_shell.ts": "bdbf8b79284b70b7ead38ae8213cb3d8ccc60dcb8a754156f0b3b82e9585c8e5", + "src/actions/execution.ts": "0b57c7cf9e3f09fe896be89724687cb9faef2149323ee08656a105fd6666d287" + }, + "protocolReferences": [ + "https://github.com/paulmillr/noble-hashes/blob/main/src/sha3.ts", + "https://github.com/Daninet/hash-wasm/blob/master/lib/WASMInterface.ts", + "https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/optimizing-latency" + ], + "runtimes": { + "bun": { + "runtime": "Bun 1.2.20", + "cpu": "Apple M3 Max", + "date": "2026-10-06T21:36:18.641Z", + "samples": 11, + "method": "Rotating paired variants; three warmup samples discarded; medians/p90 per complete action; fresh nonces and signatures; mock transports materialize outgoing UTF-8; no network latency.", + "candidateSearch": [ + { + "scenario": "checkpoint_1/noble_full", + "medianUs": 2.2386083333333393, + "p90Us": 2.2761208333333345 + }, + { + "scenario": "checkpoint_1/noble_resume", + "medianUs": 2.148234733333341, + "p90Us": 2.228222199999997 + }, + { + "scenario": "checkpoint_1/noble_cold", + "medianUs": 2.3855277666666703, + "p90Us": 2.420422233333329 + }, + { + "scenario": "checkpoint_1/wasm_full", + "medianUs": 0.44530556666666904, + "p90Us": 0.5133124999999988 + }, + { + "scenario": "checkpoint_1/wasm_resume", + "medianUs": 0.5404027666666631, + "p90Us": 0.5977611000000024 + }, + { + "scenario": "checkpoint_1/wasm_cold", + "medianUs": 0.9553791666666711, + "p90Us": 0.9764277999999952 + }, + { + "scenario": "atoms_1/current", + "medianUs": 0.22955556666665872, + "p90Us": 0.25668333333333065 + }, + { + "scenario": "atoms_1/single_ascii", + "medianUs": 0.2792583333333217, + "p90Us": 0.30656666666664023 + }, + { + "scenario": "atoms_1/cached_keys", + "medianUs": 0.35929719999999177, + "p90Us": 0.36786109999999705 + }, + { + "scenario": "wire_1/full_json", + "medianUs": 1.0989652666666492, + "p90Us": 1.1070875000000038 + }, + { + "scenario": "wire_1/action_fragment", + "medianUs": 0.05808333333331272, + "p90Us": 0.06150556666668005 + }, + { + "scenario": "wire_1/full_json_utf8", + "medianUs": 1.2626208333333429, + "p90Us": 1.2885916666666768 + }, + { + "scenario": "wire_1/action_fragment_utf8", + "medianUs": 0.17196110000001377, + "p90Us": 0.19699999999999515 + }, + { + "scenario": "checkpoint_10/noble_full", + "medianUs": 7.712916666666691, + "p90Us": 8.094041666666877 + }, + { + "scenario": "checkpoint_10/noble_resume", + "medianUs": 2.1857776666665814, + "p90Us": 2.710652666666647 + }, + { + "scenario": "checkpoint_10/noble_cold", + "medianUs": 8.08809733333328, + "p90Us": 8.487013999999968 + }, + { + "scenario": "checkpoint_10/wasm_full", + "medianUs": 1.1284026666668676, + "p90Us": 1.1769583333331564 + }, + { + "scenario": "checkpoint_10/wasm_resume", + "medianUs": 0.5621943333335366, + "p90Us": 0.5786943333335633 + }, + { + "scenario": "checkpoint_10/wasm_cold", + "medianUs": 1.5319860000002639, + "p90Us": 1.6088890000000902 + }, + { + "scenario": "atoms_10/current", + "medianUs": 1.4662083333335734, + "p90Us": 1.5163886666665956 + }, + { + "scenario": "atoms_10/single_ascii", + "medianUs": 1.6990693333330757, + "p90Us": 1.8679166666667395 + }, + { + "scenario": "atoms_10/cached_keys", + "medianUs": 2.414208333333287, + "p90Us": 2.448958333333394 + }, + { + "scenario": "wire_10/full_json", + "medianUs": 4.897611000000022, + "p90Us": 5.047027999999955 + }, + { + "scenario": "wire_10/action_fragment", + "medianUs": 0.06127766666668322, + "p90Us": 0.07013900000007804 + }, + { + "scenario": "wire_10/full_json_utf8", + "medianUs": 5.406458333333528, + "p90Us": 5.420346999999917 + }, + { + "scenario": "wire_10/action_fragment_utf8", + "medianUs": 0.3852083333331393, + "p90Us": 0.4682223333332634 + }, + { + "scenario": "checkpoint_100/noble_full", + "medianUs": 58.665970000001835, + "p90Us": 59.44014000000001 + }, + { + "scenario": "checkpoint_100/noble_resume", + "medianUs": 2.2509700000015678, + "p90Us": 2.3111100000005536 + }, + { + "scenario": "checkpoint_100/noble_cold", + "medianUs": 59.78319333333578, + "p90Us": 60.63610999999886 + }, + { + "scenario": "checkpoint_100/wasm_full", + "medianUs": 7.373056666668465, + "p90Us": 7.38805666666849 + }, + { + "scenario": "checkpoint_100/wasm_resume", + "medianUs": 0.6340266666666139, + "p90Us": 0.652776666668918 + }, + { + "scenario": "checkpoint_100/wasm_cold", + "medianUs": 7.830140000002454, + "p90Us": 7.871666666666594 + }, + { + "scenario": "atoms_100/current", + "medianUs": 14.944720000000112, + "p90Us": 15.225556666664488 + }, + { + "scenario": "atoms_100/single_ascii", + "medianUs": 16.639029999999668, + "p90Us": 16.875696666669075 + }, + { + "scenario": "atoms_100/cached_keys", + "medianUs": 23.057916666666642, + "p90Us": 23.31361000000167 + }, + { + "scenario": "wire_100/full_json", + "medianUs": 41.963609999999484, + "p90Us": 42.59069333333476 + }, + { + "scenario": "wire_100/action_fragment", + "medianUs": 0.07235999999769167, + "p90Us": 0.1631966666658021 + }, + { + "scenario": "wire_100/full_json_utf8", + "medianUs": 43.40583333333294, + "p90Us": 45.297360000001085 + }, + { + "scenario": "wire_100/action_fragment_utf8", + "medianUs": 0.8501399999992524, + "p90Us": 1.1011133333340695 + }, + { + "scenario": "checkpoint_1000/noble_full", + "medianUs": 547.4424999999974, + "p90Us": 560.3016600000046 + }, + { + "scenario": "checkpoint_1000/noble_resume", + "medianUs": 2.4400000000059663, + "p90Us": 2.5883299999986775 + }, + { + "scenario": "checkpoint_1000/noble_cold", + "medianUs": 542.8612500000054, + "p90Us": 560.4349999999977 + }, + { + "scenario": "checkpoint_1000/wasm_full", + "medianUs": 68.93500000000131, + "p90Us": 70.02166999998735 + }, + { + "scenario": "checkpoint_1000/wasm_resume", + "medianUs": 0.8374999999978172, + "p90Us": 0.8895800000027521 + }, + { + "scenario": "checkpoint_1000/wasm_cold", + "medianUs": 69.17999999999665, + "p90Us": 70.08583000000726 + }, + { + "scenario": "atoms_1000/current", + "medianUs": 145.96332999999504, + "p90Us": 150.54125000000568 + }, + { + "scenario": "atoms_1000/single_ascii", + "medianUs": 153.43208000000232, + "p90Us": 155.9166699999878 + }, + { + "scenario": "atoms_1000/cached_keys", + "medianUs": 240.21124999999302, + "p90Us": 243.96042000000307 + }, + { + "scenario": "wire_1000/full_json", + "medianUs": 399.04082999999446, + "p90Us": 403.82792000000336 + }, + { + "scenario": "wire_1000/action_fragment", + "medianUs": 0.09915999999066116, + "p90Us": 0.12416999999913969 + }, + { + "scenario": "wire_1000/full_json_utf8", + "medianUs": 406.7933300000004, + "p90Us": 420.17667000000074 + }, + { + "scenario": "wire_1000/action_fragment_utf8", + "medianUs": 2.8162499999962165, + "p90Us": 7.3458299999947485 + } + ], + "productionHashComparisons": [ + { + "scenario": "hash_wasm_1", + "ownership": "owned", + "before": { + "medianUs": 0.4354, + "p90Us": 0.4562 + }, + "after": { + "medianUs": 0.4424, + "p90Us": 0.4835 + }, + "speedup": 0.984 + }, + { + "scenario": "hash_wasm_1", + "ownership": "mutable", + "before": { + "medianUs": 0.6146, + "p90Us": 0.6471 + }, + "after": { + "medianUs": 0.6259, + "p90Us": 0.6856 + }, + "speedup": 0.982 + }, + { + "scenario": "hash_wasm_4", + "ownership": "owned", + "before": { + "medianUs": 0.6656, + "p90Us": 0.6778 + }, + "after": { + "medianUs": 0.6634, + "p90Us": 0.6838 + }, + "speedup": 1.003 + }, + { + "scenario": "hash_wasm_4", + "ownership": "mutable", + "before": { + "medianUs": 1.1692, + "p90Us": 1.467 + }, + "after": { + "medianUs": 1.1935, + "p90Us": 1.2361 + }, + "speedup": 0.98 + }, + { + "scenario": "hash_wasm_8", + "ownership": "owned", + "before": { + "medianUs": 0.8883, + "p90Us": 0.9005 + }, + "after": { + "medianUs": 0.6022, + "p90Us": 0.6441 + }, + "speedup": 1.475 + }, + { + "scenario": "hash_wasm_8", + "ownership": "mutable", + "before": { + "medianUs": 1.8027, + "p90Us": 1.8362 + }, + "after": { + "medianUs": 1.8833, + "p90Us": 1.9057 + }, + "speedup": 0.957 + }, + { + "scenario": "hash_wasm_10", + "ownership": "owned", + "before": { + "medianUs": 1.1037, + "p90Us": 1.1302 + }, + "after": { + "medianUs": 0.6051, + "p90Us": 0.6409 + }, + "speedup": 1.824 + }, + { + "scenario": "hash_wasm_10", + "ownership": "mutable", + "before": { + "medianUs": 2.2673, + "p90Us": 2.3469 + }, + "after": { + "medianUs": 2.302, + "p90Us": 2.3483 + }, + "speedup": 0.985 + }, + { + "scenario": "hash_wasm_100", + "ownership": "owned", + "before": { + "medianUs": 7.135, + "p90Us": 7.195 + }, + "after": { + "medianUs": 0.6083, + "p90Us": 0.7033 + }, + "speedup": 11.729 + }, + { + "scenario": "hash_wasm_100", + "ownership": "mutable", + "before": { + "medianUs": 21.6492, + "p90Us": 24.3933 + }, + "after": { + "medianUs": 21.2842, + "p90Us": 22.9792 + }, + "speedup": 1.017 + }, + { + "scenario": "hash_wasm_1000", + "ownership": "owned", + "before": { + "medianUs": 67.5383, + "p90Us": 68.0242 + }, + "after": { + "medianUs": 0.6892, + "p90Us": 1.1 + }, + "speedup": 98.001 + }, + { + "scenario": "hash_wasm_1000", + "ownership": "mutable", + "before": { + "medianUs": 210.5967, + "p90Us": 212.7625 + }, + "after": { + "medianUs": 210.9417, + "p90Us": 214.9408 + }, + "speedup": 0.998 + }, + { + "scenario": "hash_noble_1", + "ownership": "owned", + "before": { + "medianUs": 2.0876, + "p90Us": 2.1334 + }, + "after": { + "medianUs": 2.1073, + "p90Us": 2.4395 + }, + "speedup": 0.991 + }, + { + "scenario": "hash_noble_1", + "ownership": "mutable", + "before": { + "medianUs": 2.3055, + "p90Us": 2.7234 + }, + "after": { + "medianUs": 2.3473, + "p90Us": 2.7263 + }, + "speedup": 0.982 + }, + { + "scenario": "hash_noble_4", + "ownership": "owned", + "before": { + "medianUs": 3.7622, + "p90Us": 5.0181 + }, + "after": { + "medianUs": 3.7631, + "p90Us": 3.8652 + }, + "speedup": 1.0 + }, + { + "scenario": "hash_noble_4", + "ownership": "mutable", + "before": { + "medianUs": 4.471, + "p90Us": 4.5523 + }, + "after": { + "medianUs": 4.4493, + "p90Us": 4.5526 + }, + "speedup": 1.005 + }, + { + "scenario": "hash_noble_8", + "ownership": "owned", + "before": { + "medianUs": 5.5952, + "p90Us": 5.6519 + }, + "after": { + "medianUs": 2.0431, + "p90Us": 2.0907 + }, + "speedup": 2.739 + }, + { + "scenario": "hash_noble_8", + "ownership": "mutable", + "before": { + "medianUs": 6.8495, + "p90Us": 6.9027 + }, + "after": { + "medianUs": 6.8787, + "p90Us": 7.0149 + }, + "speedup": 0.996 + }, + { + "scenario": "hash_noble_10", + "ownership": "owned", + "before": { + "medianUs": 7.2071, + "p90Us": 7.3027 + }, + "after": { + "medianUs": 2.0269, + "p90Us": 2.0712 + }, + "speedup": 3.556 + }, + { + "scenario": "hash_noble_10", + "ownership": "mutable", + "before": { + "medianUs": 8.7763, + "p90Us": 9.0312 + }, + "after": { + "medianUs": 8.7602, + "p90Us": 11.8284 + }, + "speedup": 1.002 + }, + { + "scenario": "hash_noble_100", + "ownership": "owned", + "before": { + "medianUs": 57.39, + "p90Us": 58.1458 + }, + "after": { + "medianUs": 2.2392, + "p90Us": 2.3683 + }, + "speedup": 25.63 + }, + { + "scenario": "hash_noble_100", + "ownership": "mutable", + "before": { + "medianUs": 71.88, + "p90Us": 73.13 + }, + "after": { + "medianUs": 72.3925, + "p90Us": 73.1442 + }, + "speedup": 0.993 + }, + { + "scenario": "hash_noble_1000", + "ownership": "owned", + "before": { + "medianUs": 538.7533, + "p90Us": 543.6025 + }, + "after": { + "medianUs": 2.2333, + "p90Us": 2.3892 + }, + "speedup": 241.232 + }, + { + "scenario": "hash_noble_1000", + "ownership": "mutable", + "before": { + "medianUs": 680.7383, + "p90Us": 691.2425 + }, + "after": { + "medianUs": 678.0025, + "p90Us": 683.4158 + }, + "speedup": 1.004 + } + ], + "productionExecutionComparisons": [ + { + "scenario": "execute_stub_memory_1", + "before": { + "medianUs": 3.7439, + "p90Us": 4.5464 + }, + "after": { + "medianUs": 3.7244, + "p90Us": 4.4314 + }, + "speedup": 1.005, + "timeReductionPercent": 0.52 + }, + { + "scenario": "execute_stub_http_1", + "before": { + "medianUs": 6.5611, + "p90Us": 7.7542 + }, + "after": { + "medianUs": 6.6781, + "p90Us": 6.9764 + }, + "speedup": 0.982, + "timeReductionPercent": -1.78 + }, + { + "scenario": "execute_stub_ws_1", + "before": { + "medianUs": 8.7831, + "p90Us": 9.6336 + }, + "after": { + "medianUs": 8.7233, + "p90Us": 10.37 + }, + "speedup": 1.007, + "timeReductionPercent": 0.68 + }, + { + "scenario": "execute_stub_memory_8", + "before": { + "medianUs": 3.7197, + "p90Us": 4.1242 + }, + "after": { + "medianUs": 3.5983, + "p90Us": 4.0567 + }, + "speedup": 1.034, + "timeReductionPercent": 3.26 + }, + { + "scenario": "execute_stub_http_8", + "before": { + "medianUs": 10.285, + "p90Us": 10.4861 + }, + "after": { + "medianUs": 6.7531, + "p90Us": 7.0911 + }, + "speedup": 1.523, + "timeReductionPercent": 34.34 + }, + { + "scenario": "execute_stub_ws_8", + "before": { + "medianUs": 12.4622, + "p90Us": 13.0478 + }, + "after": { + "medianUs": 9.0711, + "p90Us": 9.8781 + }, + "speedup": 1.374, + "timeReductionPercent": 27.21 + }, + { + "scenario": "execute_stub_memory_100", + "before": { + "medianUs": 10.0811, + "p90Us": 10.2964 + }, + "after": { + "medianUs": 3.5181, + "p90Us": 3.7547 + }, + "speedup": 2.866, + "timeReductionPercent": 65.1 + }, + { + "scenario": "execute_stub_http_100", + "before": { + "medianUs": 54.3936, + "p90Us": 55.3336 + }, + "after": { + "medianUs": 7.7103, + "p90Us": 8.6725 + }, + "speedup": 7.055, + "timeReductionPercent": 85.83 + }, + { + "scenario": "execute_stub_ws_100", + "before": { + "medianUs": 56.2775, + "p90Us": 58.4644 + }, + "after": { + "medianUs": 9.9756, + "p90Us": 14.6647 + }, + "speedup": 5.642, + "timeReductionPercent": 82.27 + }, + { + "scenario": "execute_stub_memory_1000", + "before": { + "medianUs": 73.5925, + "p90Us": 74.8733 + }, + "after": { + "medianUs": 4.08, + "p90Us": 4.2817 + }, + "speedup": 18.037, + "timeReductionPercent": 94.46 + }, + { + "scenario": "execute_stub_http_1000", + "before": { + "medianUs": 489.7942, + "p90Us": 509.6767 + }, + "after": { + "medianUs": 13.0308, + "p90Us": 16.12 + }, + "speedup": 37.587, + "timeReductionPercent": 97.34 + }, + { + "scenario": "execute_stub_ws_1000", + "before": { + "medianUs": 500.8717, + "p90Us": 526.8683 + }, + "after": { + "medianUs": 16.5933, + "p90Us": 27.2225 + }, + "speedup": 30.185, + "timeReductionPercent": 96.69 + }, + { + "scenario": "execute_wasm_memory_1", + "before": { + "medianUs": 50.3997, + "p90Us": 51.0703 + }, + "after": { + "medianUs": 50.3286, + "p90Us": 51.6042 + }, + "speedup": 1.001, + "timeReductionPercent": 0.14 + }, + { + "scenario": "execute_wasm_http_1", + "before": { + "medianUs": 51.8708, + "p90Us": 52.9619 + }, + "after": { + "medianUs": 52.3889, + "p90Us": 52.7367 + }, + "speedup": 0.99, + "timeReductionPercent": -1.0 + }, + { + "scenario": "execute_wasm_ws_1", + "before": { + "medianUs": 53.4614, + "p90Us": 54.7819 + }, + "after": { + "medianUs": 53.8222, + "p90Us": 54.7956 + }, + "speedup": 0.993, + "timeReductionPercent": -0.67 + }, + { + "scenario": "execute_wasm_memory_8", + "before": { + "medianUs": 48.6211, + "p90Us": 49.8147 + }, + "after": { + "medianUs": 48.6931, + "p90Us": 49.0947 + }, + "speedup": 0.999, + "timeReductionPercent": -0.15 + }, + { + "scenario": "execute_wasm_http_8", + "before": { + "medianUs": 58.3069, + "p90Us": 59.47 + }, + "after": { + "medianUs": 55.3103, + "p90Us": 56.885 + }, + "speedup": 1.054, + "timeReductionPercent": 5.14 + }, + { + "scenario": "execute_wasm_ws_8", + "before": { + "medianUs": 59.2583, + "p90Us": 60.6456 + }, + "after": { + "medianUs": 56.8856, + "p90Us": 60.6072 + }, + "speedup": 1.042, + "timeReductionPercent": 4.0 + }, + { + "scenario": "execute_wasm_memory_100", + "before": { + "medianUs": 56.7739, + "p90Us": 56.955 + }, + "after": { + "medianUs": 50.415, + "p90Us": 50.6928 + }, + "speedup": 1.126, + "timeReductionPercent": 11.2 + }, + { + "scenario": "execute_wasm_http_100", + "before": { + "medianUs": 102.6536, + "p90Us": 103.6725 + }, + "after": { + "medianUs": 54.8719, + "p90Us": 56.0986 + }, + "speedup": 1.871, + "timeReductionPercent": 46.55 + }, + { + "scenario": "execute_wasm_ws_100", + "before": { + "medianUs": 101.5644, + "p90Us": 104.8986 + }, + "after": { + "medianUs": 55.8478, + "p90Us": 59.2833 + }, + "speedup": 1.819, + "timeReductionPercent": 45.01 + }, + { + "scenario": "execute_wasm_memory_1000", + "before": { + "medianUs": 119.0958, + "p90Us": 121.9 + }, + "after": { + "medianUs": 50.4233, + "p90Us": 52.6642 + }, + "speedup": 2.362, + "timeReductionPercent": 57.66 + }, + { + "scenario": "execute_wasm_http_1000", + "before": { + "medianUs": 516.5667, + "p90Us": 536.9292 + }, + "after": { + "medianUs": 58.055, + "p90Us": 59.6242 + }, + "speedup": 8.898, + "timeReductionPercent": 88.76 + }, + { + "scenario": "execute_wasm_ws_1000", + "before": { + "medianUs": 519.5267, + "p90Us": 522.9892 + }, + "after": { + "medianUs": 61.7208, + "p90Us": 70.1175 + }, + "speedup": 8.417, + "timeReductionPercent": 88.12 + } + ], + "rawReports": ["/tmp/hyperliquid-search-bun.json", "/tmp/hyperliquid-verify-bun.json"], + "stagedSigningComparisons": { + "date": "2026-10-06T21:55:09.864Z", + "samples": 11, + "method": "Sign a fresh request, then submit it; reuse the builder payload; rotating paired variants; real outgoing UTF-8 materialization; no network latency. Original unoptimized working tree is the baseline.", + "results": [ + { + "scenario": "staged_stub_memory_1", + "before": { + "medianUs": 6.4506, + "p90Us": 8.3281 + }, + "after": { + "medianUs": 4.7469, + "p90Us": 5.7225 + }, + "speedup": 1.359, + "timeReductionPercent": 26.41 + }, + { + "scenario": "staged_stub_http_1", + "before": { + "medianUs": 9.0614, + "p90Us": 9.5189 + }, + "after": { + "medianUs": 7.52, + "p90Us": 8.5431 + }, + "speedup": 1.205, + "timeReductionPercent": 17.01 + }, + { + "scenario": "staged_stub_ws_1", + "before": { + "medianUs": 11.3547, + "p90Us": 11.9297 + }, + "after": { + "medianUs": 9.2819, + "p90Us": 10.2428 + }, + "speedup": 1.223, + "timeReductionPercent": 18.25 + }, + { + "scenario": "staged_stub_memory_8", + "before": { + "medianUs": 11.7397, + "p90Us": 13.0317 + }, + "after": { + "medianUs": 4.3336, + "p90Us": 4.4572 + }, + "speedup": 2.709, + "timeReductionPercent": 63.09 + }, + { + "scenario": "staged_stub_http_8", + "before": { + "medianUs": 19.1964, + "p90Us": 20.6039 + }, + "after": { + "medianUs": 8.7025, + "p90Us": 9.3464 + }, + "speedup": 2.206, + "timeReductionPercent": 54.67 + }, + { + "scenario": "staged_stub_ws_8", + "before": { + "medianUs": 20.7625, + "p90Us": 23.2972 + }, + "after": { + "medianUs": 10.4092, + "p90Us": 11.5014 + }, + "speedup": 1.995, + "timeReductionPercent": 49.87 + }, + { + "scenario": "staged_stub_memory_100", + "before": { + "medianUs": 89.8944, + "p90Us": 90.9842 + }, + "after": { + "medianUs": 4.13, + "p90Us": 4.5386 + }, + "speedup": 21.766, + "timeReductionPercent": 95.41 + }, + { + "scenario": "staged_stub_http_100", + "before": { + "medianUs": 134.9839, + "p90Us": 140.1947 + }, + "after": { + "medianUs": 9.2444, + "p90Us": 11.9636 + }, + "speedup": 14.602, + "timeReductionPercent": 93.15 + }, + { + "scenario": "staged_stub_ws_100", + "before": { + "medianUs": 137.1147, + "p90Us": 142.7456 + }, + "after": { + "medianUs": 12.3319, + "p90Us": 15.4314 + }, + "speedup": 11.119, + "timeReductionPercent": 91.01 + }, + { + "scenario": "staged_stub_memory_1000", + "before": { + "medianUs": 913.0175, + "p90Us": 942.6717 + }, + "after": { + "medianUs": 4.9092, + "p90Us": 5.42 + }, + "speedup": 185.982, + "timeReductionPercent": 99.46 + }, + { + "scenario": "staged_stub_http_1000", + "before": { + "medianUs": 1333.67, + "p90Us": 1347.4033 + }, + "after": { + "medianUs": 17.0083, + "p90Us": 23.5575 + }, + "speedup": 78.413, + "timeReductionPercent": 98.72 + }, + { + "scenario": "staged_stub_ws_1000", + "before": { + "medianUs": 1330.1608, + "p90Us": 1369.6117 + }, + "after": { + "medianUs": 25.7483, + "p90Us": 32.6875 + }, + "speedup": 51.66, + "timeReductionPercent": 98.06 + }, + { + "scenario": "staged_wasm_memory_1", + "before": { + "medianUs": 54.2108, + "p90Us": 55.1022 + }, + "after": { + "medianUs": 51.535, + "p90Us": 52.9103 + }, + "speedup": 1.052, + "timeReductionPercent": 4.94 + }, + { + "scenario": "staged_wasm_http_1", + "before": { + "medianUs": 58.3411, + "p90Us": 63.0842 + }, + "after": { + "medianUs": 55.9958, + "p90Us": 57.0656 + }, + "speedup": 1.042, + "timeReductionPercent": 4.02 + }, + { + "scenario": "staged_wasm_ws_1", + "before": { + "medianUs": 59.8867, + "p90Us": 60.8197 + }, + "after": { + "medianUs": 57.6611, + "p90Us": 58.7986 + }, + "speedup": 1.039, + "timeReductionPercent": 3.72 + }, + { + "scenario": "staged_wasm_memory_8", + "before": { + "medianUs": 59.4769, + "p90Us": 62.4075 + }, + "after": { + "medianUs": 50.7186, + "p90Us": 52.2786 + }, + "speedup": 1.173, + "timeReductionPercent": 14.73 + }, + { + "scenario": "staged_wasm_http_8", + "before": { + "medianUs": 64.2831, + "p90Us": 67.1614 + }, + "after": { + "medianUs": 53.9028, + "p90Us": 55.5419 + }, + "speedup": 1.193, + "timeReductionPercent": 16.15 + }, + { + "scenario": "staged_wasm_ws_8", + "before": { + "medianUs": 65.0286, + "p90Us": 69.4744 + }, + "after": { + "medianUs": 55.7831, + "p90Us": 56.5511 + }, + "speedup": 1.166, + "timeReductionPercent": 14.22 + }, + { + "scenario": "staged_wasm_memory_100", + "before": { + "medianUs": 134.9061, + "p90Us": 135.5664 + }, + "after": { + "medianUs": 49.4981, + "p90Us": 49.8525 + }, + "speedup": 2.725, + "timeReductionPercent": 63.31 + }, + { + "scenario": "staged_wasm_http_100", + "before": { + "medianUs": 189.6217, + "p90Us": 200.0178 + }, + "after": { + "medianUs": 58.2206, + "p90Us": 59.4475 + }, + "speedup": 3.257, + "timeReductionPercent": 69.3 + }, + { + "scenario": "staged_wasm_ws_100", + "before": { + "medianUs": 189.6358, + "p90Us": 195.3831 + }, + "after": { + "medianUs": 58.9644, + "p90Us": 60.6108 + }, + "speedup": 3.216, + "timeReductionPercent": 68.91 + }, + { + "scenario": "staged_wasm_memory_1000", + "before": { + "medianUs": 903.5967, + "p90Us": 923.6542 + }, + "after": { + "medianUs": 50.4, + "p90Us": 52.1917 + }, + "speedup": 17.929, + "timeReductionPercent": 94.42 + }, + { + "scenario": "staged_wasm_http_1000", + "before": { + "medianUs": 1292.575, + "p90Us": 1311.515 + }, + "after": { + "medianUs": 60.0917, + "p90Us": 67.6442 + }, + "speedup": 21.51, + "timeReductionPercent": 95.35 + }, + { + "scenario": "staged_wasm_ws_1000", + "before": { + "medianUs": 1304.8483, + "p90Us": 1322.6308 + }, + "after": { + "medianUs": 65.445, + "p90Us": 74.0758 + }, + "speedup": 19.938, + "timeReductionPercent": 94.98 + } + ], + "rawReport": "/tmp/hyperliquid-staged-bun.json" + } + }, + "node": { + "runtime": "Node 24.6.0", + "cpu": "Apple M3 Max", + "date": "2026-10-06T21:36:53.129Z", + "samples": 11, + "method": "Rotating paired variants; three warmup samples discarded; medians/p90 per complete action; fresh nonces and signatures; mock transports materialize outgoing UTF-8; no network latency.", + "candidateSearch": [ + { + "scenario": "checkpoint_1/noble_full", + "medianUs": 13.973148599999998, + "p90Us": 15.427334733333373 + }, + { + "scenario": "checkpoint_1/noble_resume", + "medianUs": 11.28019859999998, + "p90Us": 13.59813609999995 + }, + { + "scenario": "checkpoint_1/noble_cold", + "medianUs": 15.670872233333283, + "p90Us": 17.688083333333328 + }, + { + "scenario": "checkpoint_1/wasm_full", + "medianUs": 1.368675000000015, + "p90Us": 2.734965266666647 + }, + { + "scenario": "checkpoint_1/wasm_resume", + "medianUs": 2.2263847333333007, + "p90Us": 5.17202640000008 + }, + { + "scenario": "checkpoint_1/wasm_cold", + "medianUs": 3.7143291666666602, + "p90Us": 6.70214723333332 + }, + { + "scenario": "atoms_1/current", + "medianUs": 1.3976486333333014, + "p90Us": 1.7572874999999233 + }, + { + "scenario": "atoms_1/single_ascii", + "medianUs": 1.3220527999999225, + "p90Us": 2.086415266666639 + }, + { + "scenario": "atoms_1/cached_keys", + "medianUs": 1.1582500000000437, + "p90Us": 3.273988899999919 + }, + { + "scenario": "wire_1/full_json", + "medianUs": 2.326594433333351, + "p90Us": 2.9467916666667104 + }, + { + "scenario": "wire_1/action_fragment", + "medianUs": 0.09217223333337946, + "p90Us": 1.0998597333332631 + }, + { + "scenario": "wire_1/full_json_utf8", + "medianUs": 4.190205533333331, + "p90Us": 8.018609733333262 + }, + { + "scenario": "wire_1/action_fragment_utf8", + "medianUs": 2.6955416666666374, + "p90Us": 4.3128694666667196 + }, + { + "scenario": "checkpoint_10/noble_full", + "medianUs": 50.47587500000009, + "p90Us": 67.4527776666667 + }, + { + "scenario": "checkpoint_10/noble_resume", + "medianUs": 8.86595833333437, + "p90Us": 29.069097333333048 + }, + { + "scenario": "checkpoint_10/noble_cold", + "medianUs": 52.95334733333342, + "p90Us": 67.94052766666694 + }, + { + "scenario": "checkpoint_10/wasm_full", + "medianUs": 1.4490969999994074, + "p90Us": 4.195403000000321 + }, + { + "scenario": "checkpoint_10/wasm_resume", + "medianUs": 0.8492499999993015, + "p90Us": 2.591763666667248 + }, + { + "scenario": "checkpoint_10/wasm_cold", + "medianUs": 2.3631803333325174, + "p90Us": 20.399666666666842 + }, + { + "scenario": "atoms_10/current", + "medianUs": 3.015208333334158, + "p90Us": 14.330111333333965 + }, + { + "scenario": "atoms_10/single_ascii", + "medianUs": 3.3705973333332317, + "p90Us": 18.14669433333256 + }, + { + "scenario": "atoms_10/cached_keys", + "medianUs": 12.688361000000441, + "p90Us": 28.442485999999917 + }, + { + "scenario": "wire_10/full_json", + "medianUs": 4.002597333332233, + "p90Us": 28.71595833333413 + }, + { + "scenario": "wire_10/action_fragment", + "medianUs": 0.1050416666669965, + "p90Us": 0.11454166666711292 + }, + { + "scenario": "wire_10/full_json_utf8", + "medianUs": 6.2987083333331, + "p90Us": 22.020972000000864 + }, + { + "scenario": "wire_10/action_fragment_utf8", + "medianUs": 1.6436529999991762, + "p90Us": 16.903944333333737 + }, + { + "scenario": "checkpoint_100/noble_full", + "medianUs": 373.62930666667427, + "p90Us": 471.823473333352 + }, + { + "scenario": "checkpoint_100/noble_resume", + "medianUs": 4.436113333334409, + "p90Us": 4.474166666671711 + }, + { + "scenario": "checkpoint_100/noble_cold", + "medianUs": 301.4861100000174, + "p90Us": 536.4808333333349 + }, + { + "scenario": "checkpoint_100/wasm_full", + "medianUs": 9.334030000003017, + "p90Us": 81.12722333333902 + }, + { + "scenario": "checkpoint_100/wasm_resume", + "medianUs": 0.8504166666534729, + "p90Us": 0.8937500000077611 + }, + { + "scenario": "checkpoint_100/wasm_cold", + "medianUs": 9.659723333340176, + "p90Us": 17.381806666671764 + }, + { + "scenario": "atoms_100/current", + "medianUs": 28.584719999998924, + "p90Us": 135.98403000001176 + }, + { + "scenario": "atoms_100/single_ascii", + "medianUs": 29.147083333349048, + "p90Us": 39.63083333333392 + }, + { + "scenario": "atoms_100/cached_keys", + "medianUs": 38.62791666668878, + "p90Us": 39.431806666664976 + }, + { + "scenario": "wire_100/full_json", + "medianUs": 20.549723333339596, + "p90Us": 23.30680333332566 + }, + { + "scenario": "wire_100/action_fragment", + "medianUs": 0.0995833333339154, + "p90Us": 0.1070833333506016 + }, + { + "scenario": "wire_100/full_json_utf8", + "medianUs": 29.13527666666293, + "p90Us": 33.31166666665619 + }, + { + "scenario": "wire_100/action_fragment_utf8", + "medianUs": 8.555970000015805, + "p90Us": 9.694166666668025 + }, + { + "scenario": "checkpoint_1000/noble_full", + "medianUs": 1012.6825000000099, + "p90Us": 1037.5624999999854 + }, + { + "scenario": "checkpoint_1000/noble_resume", + "medianUs": 3.589590000046883, + "p90Us": 4.607909999976982 + }, + { + "scenario": "checkpoint_1000/noble_cold", + "medianUs": 1019.8366700000042, + "p90Us": 1058.7741700000333 + }, + { + "scenario": "checkpoint_1000/wasm_full", + "medianUs": 68.46083000004, + "p90Us": 71.07250000000931 + }, + { + "scenario": "checkpoint_1000/wasm_resume", + "medianUs": 0.7837499999732245, + "p90Us": 0.9333400000468828 + }, + { + "scenario": "checkpoint_1000/wasm_cold", + "medianUs": 67.08333000002312, + "p90Us": 71.57874999997148 + }, + { + "scenario": "atoms_1000/current", + "medianUs": 209.36666000001424, + "p90Us": 216.192499999961 + }, + { + "scenario": "atoms_1000/single_ascii", + "medianUs": 216.57083999998576, + "p90Us": 224.20208999996248 + }, + { + "scenario": "atoms_1000/cached_keys", + "medianUs": 284.96791999998095, + "p90Us": 287.76208999996015 + }, + { + "scenario": "wire_1000/full_json", + "medianUs": 164.30082999999286, + "p90Us": 170.9883300000365 + }, + { + "scenario": "wire_1000/action_fragment", + "medianUs": 0.08541000002878718, + "p90Us": 0.14750000002095476 + }, + { + "scenario": "wire_1000/full_json_utf8", + "medianUs": 229.84666000003926, + "p90Us": 235.11749999997846 + }, + { + "scenario": "wire_1000/action_fragment_utf8", + "medianUs": 65.33708000002662, + "p90Us": 66.83124999995925 + } + ], + "productionHashComparisons": [ + { + "scenario": "hash_wasm_1", + "ownership": "owned", + "before": { + "medianUs": 0.4506, + "p90Us": 0.4542 + }, + "after": { + "medianUs": 0.4518, + "p90Us": 0.4671 + }, + "speedup": 0.997 + }, + { + "scenario": "hash_wasm_1", + "ownership": "mutable", + "before": { + "medianUs": 0.7828, + "p90Us": 0.805 + }, + "after": { + "medianUs": 0.7794, + "p90Us": 0.7847 + }, + "speedup": 1.004 + }, + { + "scenario": "hash_wasm_4", + "ownership": "owned", + "before": { + "medianUs": 0.6514, + "p90Us": 0.6624 + }, + "after": { + "medianUs": 0.6467, + "p90Us": 0.7032 + }, + "speedup": 1.007 + }, + { + "scenario": "hash_wasm_4", + "ownership": "mutable", + "before": { + "medianUs": 1.7139, + "p90Us": 1.7358 + }, + "after": { + "medianUs": 1.6879, + "p90Us": 1.765 + }, + "speedup": 1.015 + }, + { + "scenario": "hash_wasm_8", + "ownership": "owned", + "before": { + "medianUs": 0.8429, + "p90Us": 0.8645 + }, + "after": { + "medianUs": 0.6602, + "p90Us": 0.684 + }, + "speedup": 1.277 + }, + { + "scenario": "hash_wasm_8", + "ownership": "mutable", + "before": { + "medianUs": 2.7801, + "p90Us": 2.9402 + }, + "after": { + "medianUs": 2.7752, + "p90Us": 2.8694 + }, + "speedup": 1.002 + }, + { + "scenario": "hash_wasm_10", + "ownership": "owned", + "before": { + "medianUs": 1.0228, + "p90Us": 1.034 + }, + "after": { + "medianUs": 0.6862, + "p90Us": 0.7307 + }, + "speedup": 1.49 + }, + { + "scenario": "hash_wasm_10", + "ownership": "mutable", + "before": { + "medianUs": 3.4212, + "p90Us": 3.4568 + }, + "after": { + "medianUs": 3.4368, + "p90Us": 3.5802 + }, + "speedup": 0.995 + }, + { + "scenario": "hash_wasm_100", + "ownership": "owned", + "before": { + "medianUs": 6.3742, + "p90Us": 6.4875 + }, + "after": { + "medianUs": 0.69, + "p90Us": 0.7892 + }, + "speedup": 9.238 + }, + { + "scenario": "hash_wasm_100", + "ownership": "mutable", + "before": { + "medianUs": 27.9475, + "p90Us": 29.31 + }, + "after": { + "medianUs": 27.8042, + "p90Us": 28.0733 + }, + "speedup": 1.005 + }, + { + "scenario": "hash_wasm_1000", + "ownership": "owned", + "before": { + "medianUs": 60.5883, + "p90Us": 62.535 + }, + "after": { + "medianUs": 0.8483, + "p90Us": 0.885 + }, + "speedup": 71.422 + }, + { + "scenario": "hash_wasm_1000", + "ownership": "mutable", + "before": { + "medianUs": 273.1475, + "p90Us": 276.6092 + }, + "after": { + "medianUs": 275.7167, + "p90Us": 279.7683 + }, + "speedup": 0.991 + }, + { + "scenario": "hash_noble_1", + "ownership": "owned", + "before": { + "medianUs": 3.4079, + "p90Us": 3.4299 + }, + "after": { + "medianUs": 3.3834, + "p90Us": 3.4077 + }, + "speedup": 1.007 + }, + { + "scenario": "hash_noble_1", + "ownership": "mutable", + "before": { + "medianUs": 3.7164, + "p90Us": 3.7777 + }, + "after": { + "medianUs": 3.7055, + "p90Us": 3.7588 + }, + "speedup": 1.003 + }, + { + "scenario": "hash_noble_4", + "ownership": "owned", + "before": { + "medianUs": 6.5393, + "p90Us": 6.6536 + }, + "after": { + "medianUs": 6.5554, + "p90Us": 6.5971 + }, + "speedup": 0.998 + }, + { + "scenario": "hash_noble_4", + "ownership": "mutable", + "before": { + "medianUs": 7.5906, + "p90Us": 7.6769 + }, + "after": { + "medianUs": 7.4818, + "p90Us": 7.6436 + }, + "speedup": 1.015 + }, + { + "scenario": "hash_noble_8", + "ownership": "owned", + "before": { + "medianUs": 9.6166, + "p90Us": 9.7459 + }, + "after": { + "medianUs": 3.3198, + "p90Us": 3.352 + }, + "speedup": 2.897 + }, + { + "scenario": "hash_noble_8", + "ownership": "mutable", + "before": { + "medianUs": 11.583, + "p90Us": 11.8203 + }, + "after": { + "medianUs": 11.6551, + "p90Us": 11.7175 + }, + "speedup": 0.994 + }, + { + "scenario": "hash_noble_10", + "ownership": "owned", + "before": { + "medianUs": 12.5977, + "p90Us": 12.7781 + }, + "after": { + "medianUs": 3.343, + "p90Us": 3.4458 + }, + "speedup": 3.768 + }, + { + "scenario": "hash_noble_10", + "ownership": "mutable", + "before": { + "medianUs": 14.9426, + "p90Us": 15.3175 + }, + "after": { + "medianUs": 14.9974, + "p90Us": 15.2309 + }, + "speedup": 0.996 + }, + { + "scenario": "hash_noble_100", + "ownership": "owned", + "before": { + "medianUs": 98.3675, + "p90Us": 99.3808 + }, + "after": { + "medianUs": 3.3392, + "p90Us": 3.485 + }, + "speedup": 29.459 + }, + { + "scenario": "hash_noble_100", + "ownership": "mutable", + "before": { + "medianUs": 120.4383, + "p90Us": 123.7667 + }, + "after": { + "medianUs": 121.6317, + "p90Us": 124.2517 + }, + "speedup": 0.99 + }, + { + "scenario": "hash_noble_1000", + "ownership": "owned", + "before": { + "medianUs": 946.5692, + "p90Us": 956.9925 + }, + "after": { + "medianUs": 3.4758, + "p90Us": 3.585 + }, + "speedup": 272.328 + }, + { + "scenario": "hash_noble_1000", + "ownership": "mutable", + "before": { + "medianUs": 1167.56, + "p90Us": 1176.91 + }, + "after": { + "medianUs": 1170.0267, + "p90Us": 1174.7358 + }, + "speedup": 0.998 + } + ], + "productionExecutionComparisons": [ + { + "scenario": "execute_stub_memory_1", + "before": { + "medianUs": 3.2169, + "p90Us": 4.2608 + }, + "after": { + "medianUs": 3.1439, + "p90Us": 3.6494 + }, + "speedup": 1.023, + "timeReductionPercent": 2.27 + }, + { + "scenario": "execute_stub_http_1", + "before": { + "medianUs": 11.9778, + "p90Us": 12.7106 + }, + "after": { + "medianUs": 11.7222, + "p90Us": 13.6722 + }, + "speedup": 1.022, + "timeReductionPercent": 2.13 + }, + { + "scenario": "execute_stub_ws_1", + "before": { + "medianUs": 8.7083, + "p90Us": 9.8486 + }, + "after": { + "medianUs": 9.0817, + "p90Us": 9.6453 + }, + "speedup": 0.959, + "timeReductionPercent": -4.29 + }, + { + "scenario": "execute_stub_memory_8", + "before": { + "medianUs": 3.1967, + "p90Us": 3.2853 + }, + "after": { + "medianUs": 3.3786, + "p90Us": 3.6758 + }, + "speedup": 0.946, + "timeReductionPercent": -5.69 + }, + { + "scenario": "execute_stub_http_8", + "before": { + "medianUs": 14.3017, + "p90Us": 16.7283 + }, + "after": { + "medianUs": 13.6794, + "p90Us": 15.215 + }, + "speedup": 1.045, + "timeReductionPercent": 4.35 + }, + { + "scenario": "execute_stub_ws_8", + "before": { + "medianUs": 10.5775, + "p90Us": 11.1358 + }, + "after": { + "medianUs": 10.2244, + "p90Us": 11.8258 + }, + "speedup": 1.035, + "timeReductionPercent": 3.34 + }, + { + "scenario": "execute_stub_memory_100", + "before": { + "medianUs": 8.6267, + "p90Us": 8.7886 + }, + "after": { + "medianUs": 3.1067, + "p90Us": 4.3208 + }, + "speedup": 2.777, + "timeReductionPercent": 63.99 + }, + { + "scenario": "execute_stub_http_100", + "before": { + "medianUs": 42.7258, + "p90Us": 44.5881 + }, + "after": { + "medianUs": 19.6847, + "p90Us": 21.8075 + }, + "speedup": 2.171, + "timeReductionPercent": 53.93 + }, + { + "scenario": "execute_stub_ws_100", + "before": { + "medianUs": 41.1075, + "p90Us": 41.7275 + }, + "after": { + "medianUs": 17.2547, + "p90Us": 19.0131 + }, + "speedup": 2.382, + "timeReductionPercent": 58.03 + }, + { + "scenario": "execute_stub_memory_1000", + "before": { + "medianUs": 62.1642, + "p90Us": 62.9683 + }, + "after": { + "medianUs": 3.0817, + "p90Us": 3.1575 + }, + "speedup": 20.172, + "timeReductionPercent": 95.04 + }, + { + "scenario": "execute_stub_http_1000", + "before": { + "medianUs": 311.565, + "p90Us": 314.4308 + }, + "after": { + "medianUs": 76.695, + "p90Us": 78.6283 + }, + "speedup": 4.062, + "timeReductionPercent": 75.38 + }, + { + "scenario": "execute_stub_ws_1000", + "before": { + "medianUs": 322.3167, + "p90Us": 340.5683 + }, + "after": { + "medianUs": 78.5233, + "p90Us": 97.4408 + }, + "speedup": 4.105, + "timeReductionPercent": 75.64 + }, + { + "scenario": "execute_wasm_memory_1", + "before": { + "medianUs": 71.3797, + "p90Us": 72.3153 + }, + "after": { + "medianUs": 71.3558, + "p90Us": 72.5094 + }, + "speedup": 1.0, + "timeReductionPercent": 0.03 + }, + { + "scenario": "execute_wasm_http_1", + "before": { + "medianUs": 78.7544, + "p90Us": 79.7353 + }, + "after": { + "medianUs": 79.2786, + "p90Us": 81.3428 + }, + "speedup": 0.993, + "timeReductionPercent": -0.67 + }, + { + "scenario": "execute_wasm_ws_1", + "before": { + "medianUs": 76.9472, + "p90Us": 78.1272 + }, + "after": { + "medianUs": 77.0756, + "p90Us": 77.96 + }, + "speedup": 0.998, + "timeReductionPercent": -0.17 + }, + { + "scenario": "execute_wasm_memory_8", + "before": { + "medianUs": 70.6175, + "p90Us": 72.3569 + }, + "after": { + "medianUs": 71.3425, + "p90Us": 72.9958 + }, + "speedup": 0.99, + "timeReductionPercent": -1.03 + }, + { + "scenario": "execute_wasm_http_8", + "before": { + "medianUs": 80.5681, + "p90Us": 81.9 + }, + "after": { + "medianUs": 79.6781, + "p90Us": 81.0333 + }, + "speedup": 1.011, + "timeReductionPercent": 1.1 + }, + { + "scenario": "execute_wasm_ws_8", + "before": { + "medianUs": 78.3947, + "p90Us": 79.4036 + }, + "after": { + "medianUs": 77.8917, + "p90Us": 80.1064 + }, + "speedup": 1.006, + "timeReductionPercent": 0.64 + }, + { + "scenario": "execute_wasm_memory_100", + "before": { + "medianUs": 76.2331, + "p90Us": 76.8069 + }, + "after": { + "medianUs": 71.1697, + "p90Us": 73.6728 + }, + "speedup": 1.071, + "timeReductionPercent": 6.64 + }, + { + "scenario": "execute_wasm_http_100", + "before": { + "medianUs": 108.7017, + "p90Us": 110.3989 + }, + "after": { + "medianUs": 85.0764, + "p90Us": 87.5678 + }, + "speedup": 1.278, + "timeReductionPercent": 21.73 + }, + { + "scenario": "execute_wasm_ws_100", + "before": { + "medianUs": 108.2933, + "p90Us": 109.3769 + }, + "after": { + "medianUs": 84.2344, + "p90Us": 85.0275 + }, + "speedup": 1.286, + "timeReductionPercent": 22.22 + }, + { + "scenario": "execute_wasm_memory_1000", + "before": { + "medianUs": 129.2858, + "p90Us": 130.1842 + }, + "after": { + "medianUs": 70.8375, + "p90Us": 71.2242 + }, + "speedup": 1.825, + "timeReductionPercent": 45.21 + }, + { + "scenario": "execute_wasm_http_1000", + "before": { + "medianUs": 390.6983, + "p90Us": 395.9058 + }, + "after": { + "medianUs": 145.795, + "p90Us": 147.3475 + }, + "speedup": 2.68, + "timeReductionPercent": 62.68 + }, + { + "scenario": "execute_wasm_ws_1000", + "before": { + "medianUs": 391.8258, + "p90Us": 395.8867 + }, + "after": { + "medianUs": 145.125, + "p90Us": 150.2767 + }, + "speedup": 2.7, + "timeReductionPercent": 62.96 + } + ], + "rawReports": ["/tmp/hyperliquid-search-node.json", "/tmp/hyperliquid-verify-node.json"], + "stagedSigningComparisons": { + "date": "2026-10-06T21:56:25.699Z", + "samples": 11, + "method": "Sign a fresh request, then submit it; reuse the builder payload; rotating paired variants; real outgoing UTF-8 materialization; no network latency. Original unoptimized working tree is the baseline.", + "results": [ + { + "scenario": "staged_stub_memory_1", + "before": { + "medianUs": 7.2647, + "p90Us": 10.0869 + }, + "after": { + "medianUs": 4.3086, + "p90Us": 5.6539 + }, + "speedup": 1.686, + "timeReductionPercent": 40.69 + }, + { + "scenario": "staged_stub_http_1", + "before": { + "medianUs": 16.3903, + "p90Us": 17.5689 + }, + "after": { + "medianUs": 14.4931, + "p90Us": 15.8936 + }, + "speedup": 1.131, + "timeReductionPercent": 11.58 + }, + { + "scenario": "staged_stub_ws_1", + "before": { + "medianUs": 13.1636, + "p90Us": 15.2833 + }, + "after": { + "medianUs": 10.2, + "p90Us": 12.2883 + }, + "speedup": 1.291, + "timeReductionPercent": 22.51 + }, + { + "scenario": "staged_stub_memory_8", + "before": { + "medianUs": 13.6633, + "p90Us": 15.7967 + }, + "after": { + "medianUs": 4.5425, + "p90Us": 5.1619 + }, + "speedup": 3.008, + "timeReductionPercent": 66.75 + }, + { + "scenario": "staged_stub_http_8", + "before": { + "medianUs": 24.5725, + "p90Us": 26.1675 + }, + "after": { + "medianUs": 15.2139, + "p90Us": 18.0889 + }, + "speedup": 1.615, + "timeReductionPercent": 38.09 + }, + { + "scenario": "staged_stub_ws_8", + "before": { + "medianUs": 21.655, + "p90Us": 24.0839 + }, + "after": { + "medianUs": 11.7967, + "p90Us": 14.335 + }, + "speedup": 1.836, + "timeReductionPercent": 45.52 + }, + { + "scenario": "staged_stub_memory_100", + "before": { + "medianUs": 107.1153, + "p90Us": 108.7819 + }, + "after": { + "medianUs": 4.25, + "p90Us": 4.7747 + }, + "speedup": 25.204, + "timeReductionPercent": 96.03 + }, + { + "scenario": "staged_stub_http_100", + "before": { + "medianUs": 139.625, + "p90Us": 142.4089 + }, + "after": { + "medianUs": 20.0256, + "p90Us": 20.9486 + }, + "speedup": 6.972, + "timeReductionPercent": 85.66 + }, + { + "scenario": "staged_stub_ws_100", + "before": { + "medianUs": 146.0861, + "p90Us": 146.6808 + }, + "after": { + "medianUs": 18.8897, + "p90Us": 20.0478 + }, + "speedup": 7.734, + "timeReductionPercent": 87.07 + }, + { + "scenario": "staged_stub_memory_1000", + "before": { + "medianUs": 1053.0133, + "p90Us": 1069.6883 + }, + "after": { + "medianUs": 4.9092, + "p90Us": 5.905 + }, + "speedup": 214.499, + "timeReductionPercent": 99.53 + }, + { + "scenario": "staged_stub_http_1000", + "before": { + "medianUs": 1309.6975, + "p90Us": 1328.5192 + }, + "after": { + "medianUs": 82.5575, + "p90Us": 84.1933 + }, + "speedup": 15.864, + "timeReductionPercent": 93.7 + }, + { + "scenario": "staged_stub_ws_1000", + "before": { + "medianUs": 1306.2175, + "p90Us": 1341.24 + }, + "after": { + "medianUs": 80.5183, + "p90Us": 83.0858 + }, + "speedup": 16.223, + "timeReductionPercent": 93.84 + }, + { + "scenario": "staged_wasm_memory_1", + "before": { + "medianUs": 71.6661, + "p90Us": 73.0675 + }, + "after": { + "medianUs": 68.7231, + "p90Us": 70.1119 + }, + "speedup": 1.043, + "timeReductionPercent": 4.11 + }, + { + "scenario": "staged_wasm_http_1", + "before": { + "medianUs": 78.0769, + "p90Us": 80.6208 + }, + "after": { + "medianUs": 76.6072, + "p90Us": 78.3836 + }, + "speedup": 1.019, + "timeReductionPercent": 1.88 + }, + { + "scenario": "staged_wasm_ws_1", + "before": { + "medianUs": 77.4156, + "p90Us": 78.5544 + }, + "after": { + "medianUs": 75.0081, + "p90Us": 77.1406 + }, + "speedup": 1.032, + "timeReductionPercent": 3.11 + }, + { + "scenario": "staged_wasm_memory_8", + "before": { + "medianUs": 78.9239, + "p90Us": 81.4881 + }, + "after": { + "medianUs": 69.7219, + "p90Us": 70.9256 + }, + "speedup": 1.132, + "timeReductionPercent": 11.66 + }, + { + "scenario": "staged_wasm_http_8", + "before": { + "medianUs": 89.3347, + "p90Us": 91.2247 + }, + "after": { + "medianUs": 78.9444, + "p90Us": 83.0719 + }, + "speedup": 1.132, + "timeReductionPercent": 11.63 + }, + { + "scenario": "staged_wasm_ws_8", + "before": { + "medianUs": 87.6647, + "p90Us": 88.8961 + }, + "after": { + "medianUs": 76.1028, + "p90Us": 80.7844 + }, + "speedup": 1.152, + "timeReductionPercent": 13.19 + }, + { + "scenario": "staged_wasm_memory_100", + "before": { + "medianUs": 176.2431, + "p90Us": 180.6103 + }, + "after": { + "medianUs": 71.2017, + "p90Us": 73.0481 + }, + "speedup": 2.475, + "timeReductionPercent": 59.6 + }, + { + "scenario": "staged_wasm_http_100", + "before": { + "medianUs": 207.3394, + "p90Us": 208.9092 + }, + "after": { + "medianUs": 86.3897, + "p90Us": 89.9731 + }, + "speedup": 2.4, + "timeReductionPercent": 58.33 + }, + { + "scenario": "staged_wasm_ws_100", + "before": { + "medianUs": 206.2717, + "p90Us": 285.8078 + }, + "after": { + "medianUs": 83.8286, + "p90Us": 85.9128 + }, + "speedup": 2.461, + "timeReductionPercent": 59.36 + }, + { + "scenario": "staged_wasm_memory_1000", + "before": { + "medianUs": 1106.7675, + "p90Us": 1120.255 + }, + "after": { + "medianUs": 73.6008, + "p90Us": 74.6458 + }, + "speedup": 15.037, + "timeReductionPercent": 93.35 + }, + { + "scenario": "staged_wasm_http_1000", + "before": { + "medianUs": 1372.3117, + "p90Us": 1375.9967 + }, + "after": { + "medianUs": 150.5333, + "p90Us": 155.3767 + }, + "speedup": 9.116, + "timeReductionPercent": 89.03 + }, + { + "scenario": "staged_wasm_ws_1000", + "before": { + "medianUs": 1336.76, + "p90Us": 1374.89 + }, + "after": { + "medianUs": 144.6658, + "p90Us": 148.3067 + }, + "speedup": 9.24, + "timeReductionPercent": 89.18 + } + ], + "rawReport": "/tmp/hyperliquid-staged-node.json" + } + } + }, + "sourceFingerprintNote": "measuredSources identify the original measured working tree. sources identify the isolated PR implementation, including required reusable action support and excluding separately pending runtime, bounded dispatch, and batching work. Historical measurements retain their original context; isolatedPRVerification records fresh checks after scoping.", + "measuredSources": { + "src/signing/_keccak.ts": "078eb357ebe3bc511a137d094dc4e9abb6985b9730a2b2d47ac1f849991c5b95", + "src/signing/_l1.ts": "89af7fca9ef8e3783964852691a213d359b4935a5acce545640274ccbface0d5", + "src/signing/_l1Cache.ts": "7bc92c87189805db32a2c2be9c2220878dded8ebe05927fce3f707086e276a03", + "src/transport/_wire.ts": "0b4d48e0c2ba49e260a6a8454be988c769527594453c5ead5aa10e653c6925f1", + "src/transport/http/mod.ts": "707257031764e67249e221194fefbb717ace6300493e51b206efdf2a43b67455", + "src/transport/websocket/mod.ts": "78e45f6533862a8b64084863de0b597ecf612be9436dce7b6a128de950496924", + "src/transport/websocket/_dispatcher.ts": "e38bf835ac6a2ed3f5d4909723d41c458dc32d56dc6b89d873d0df69a5cb5d05", + "src/actions/_canonical.ts": "b511a9c5e368ec5a84c5c3f86d3b18be96be912f66e77aff2c6c60d6ca662a1f", + "src/api/exchange/_methods/_base/_shell.ts": "a7e7521c27466b482323a84e4e31f8e11cfce620028217d65be92796875f77df", + "src/actions/execution.ts": "8b72a509cbd765f5a71e6ccd745c35acd53fe639929ba7f4792311818cc8f678" + }, + "isolatedPRVerification": { + "offlineTests": { + "passed": 2008, + "skipped": 242, + "failed": 0, + "assertions": 33016 + }, + "build": "passed, relocated package checked on Bun and Node", + "benchmarks": { + "bunHTTP": { + "runtime": { + "node": "26.3.0", + "bun": "1.4.0", + "boringssl": "1a41b9025c2c0a37edd07ff10f6944f03e028522", + "openssl": "1.1.0", + "llhttp": "9.3.0", + "libarchive": "ded82291ab41d5e355831b96b0e1ff49e24d8939", + "mimalloc": "1803341d6241d8fa4b3f65fa68cb13a32ad92f04", + "picohttpparser": "066d2b1e9ab820703db0837a7255d92d30f0c9f5", + "uwebsockets": "97e21e5405f7bc548e2568d2095ca909b9ee9f8e", + "webkit": "447082ab6897278727b44e1ba3c326ae6e1504c3", + "zig": "04e7f6ac1e009525bc00934f20199c68f04e0a24", + "zlib": "12731092979c6d07f42da27da673a9f6c7b13586", + "tinycc": "05f0fafaa3be31e31d7b4b5c17dc60f62c991171", + "lolhtml": "725ce499aa9b71e38b7a2d0a9fbb6d7294a4079e", + "ares": "c7a3138dcfe3bb0eaaf10c0c24c36dc66dc790ab", + "libdeflate": "c8c56a20f8f621e6a966b716b31f1dedab6a41e3", + "usockets": "97e21e5405f7bc548e2568d2095ca909b9ee9f8e", + "lshpack": "8905c024b6d052f083a3d11d0a169b3c2735c8a1", + "zstd": "f8745da6ff1ad1e7bab384bd1f9d742439278e99", + "v8": "14.6.202.34-node.20", + "uv": "1.48.0", + "napi": "10", + "icu": "78.1", + "unicode": "17.0", + "sqlite": "3.54.0", + "modules": "147" + }, + "cpu": "Apple M3 Max", + "date": "2026-10-06T23:23:28.192Z", + "compareDir": "/tmp/hyperliquid-ocean-baseline", + "filter": "staged_wasm_http_1000", + "sampleCount": 11, + "sink": 0, + "results": [ + { + "scenario": "staged_wasm_http_1000", + "variant": "before", + "medianUs": 1008.6124999999998, + "p90Us": 1084.7649999999999, + "roundsUs": [ + 1008.6124999999998, 1008.6891800000001, 1025.1308199999994, 1001.7116599999997, 983.2841800000007, + 1008.2024999999987, 1010.7408400000008, 1088.8741600000003, 983.5741600000006, 999.865000000002, + 1084.7649999999999 + ] + }, + { + "scenario": "staged_wasm_http_1000", + "variant": "after", + "medianUs": 72.80250000000024, + "p90Us": 75.43081999999913, + "roundsUs": [ + 75.43081999999913, 74.64500000000044, 73.49500000000035, 71.24915999999985, 72.80250000000024, + 71.1583399999995, 73.63665999999967, 72.23915999999917, 88.11083999999937, 70.20833999999923, + 71.2758400000007 + ] + } + ] + }, + "bunWS": { + "runtime": { + "node": "26.3.0", + "bun": "1.4.0", + "boringssl": "1a41b9025c2c0a37edd07ff10f6944f03e028522", + "openssl": "1.1.0", + "llhttp": "9.3.0", + "libarchive": "ded82291ab41d5e355831b96b0e1ff49e24d8939", + "mimalloc": "1803341d6241d8fa4b3f65fa68cb13a32ad92f04", + "picohttpparser": "066d2b1e9ab820703db0837a7255d92d30f0c9f5", + "uwebsockets": "97e21e5405f7bc548e2568d2095ca909b9ee9f8e", + "webkit": "447082ab6897278727b44e1ba3c326ae6e1504c3", + "zig": "04e7f6ac1e009525bc00934f20199c68f04e0a24", + "zlib": "12731092979c6d07f42da27da673a9f6c7b13586", + "tinycc": "05f0fafaa3be31e31d7b4b5c17dc60f62c991171", + "lolhtml": "725ce499aa9b71e38b7a2d0a9fbb6d7294a4079e", + "ares": "c7a3138dcfe3bb0eaaf10c0c24c36dc66dc790ab", + "libdeflate": "c8c56a20f8f621e6a966b716b31f1dedab6a41e3", + "usockets": "97e21e5405f7bc548e2568d2095ca909b9ee9f8e", + "lshpack": "8905c024b6d052f083a3d11d0a169b3c2735c8a1", + "zstd": "f8745da6ff1ad1e7bab384bd1f9d742439278e99", + "v8": "14.6.202.34-node.20", + "uv": "1.48.0", + "napi": "10", + "icu": "78.1", + "unicode": "17.0", + "sqlite": "3.54.0", + "modules": "147" + }, + "cpu": "Apple M3 Max", + "date": "2026-10-06T23:23:49.576Z", + "compareDir": "/tmp/hyperliquid-ocean-baseline", + "filter": "staged_wasm_ws_1000", + "sampleCount": 11, + "sink": 0, + "results": [ + { + "scenario": "staged_wasm_ws_1000", + "variant": "before", + "medianUs": 988.83916, + "p90Us": 1008.3441599999992, + "roundsUs": [ + 984.8433400000005, 976.1966600000005, 996.0500000000002, 988.83916, 986.2724999999988, 1042.834999999999, + 992.43334, 1008.3441599999992, 986.0924999999997, 989.3325000000004, 983.8883399999986 + ] + }, + { + "scenario": "staged_wasm_ws_1000", + "variant": "after", + "medianUs": 82.52418000000034, + "p90Us": 94.15415999999935, + "roundsUs": [ + 93.88665999999942, 79.43832000000043, 72.86749999999984, 83.99499999999989, 71.73084000000017, + 94.15415999999935, 75.68418000000065, 82.52418000000034, 96.0349999999994, 76.44084000000021, + 87.62165999999979 + ] + } + ] + }, + "nodeHTTP": { + "runtime": { + "node": "24.6.0", + "acorn": "8.15.0", + "ada": "3.2.7", + "amaro": "1.1.1", + "ares": "1.34.5", + "brotli": "1.2.0", + "cjs_module_lexer": "2.1.0", + "cldr": "47.0", + "icu": "77.1", + "llhttp": "9.3.0", + "modules": "137", + "napi": "10", + "nbytes": "0.1.1", + "ncrypto": "0.0.1", + "nghttp2": "1.66.0", + "openssl": "3.6.5", + "simdjson": "3.13.0", + "simdutf": "6.4.0", + "sqlite": "3.50.4", + "tz": "2025a", + "undici": "7.13.0", + "unicode": "16.0", + "uv": "1.51.0", + "uvwasi": "0.0.21", + "v8": "13.6.233.10-node.24", + "zlib": "1.2.12", + "zstd": "1.5.7" + }, + "cpu": "Apple M3 Max", + "date": "2026-10-06T23:24:04.009Z", + "compareDir": "/tmp/hyperliquid-ocean-baseline", + "filter": "staged_wasm_http_1000", + "sampleCount": 11, + "sink": 0, + "results": [ + { + "scenario": "staged_wasm_http_1000", + "variant": "before", + "medianUs": 1406.34, + "p90Us": 1438.7800000000016, + "roundsUs": [ + 1422.0191599999996, 1466.4858400000003, 1438.7800000000016, 1424.64334, 1417.2800000000007, + 1390.9583199999997, 1379.44166, 1390.7441800000015, 1406.34, 1383.2258199999978, 1363.6974999999982 + ] + }, + { + "scenario": "staged_wasm_http_1000", + "variant": "after", + "medianUs": 157.7341800000022, + "p90Us": 161.58832000000075, + "roundsUs": [ + 159.47084000000018, 163.29165999999987, 161.58832000000075, 158.7808199999995, 154.81666000000132, + 156.56499999999824, 153.3366799999999, 160.195819999999, 154.72083999999995, 148.31834000000072, + 157.7341800000022 + ] + } + ] + } + } + } +} diff --git a/packages/hyperliquid/package.json b/packages/hyperliquid/package.json index fd41447c..1a5aecd1 100644 --- a/packages/hyperliquid/package.json +++ b/packages/hyperliquid/package.json @@ -32,7 +32,7 @@ "node": ">=22.12.0" }, "//exports": "Points at TypeScript sources: Bun consumes them directly, and self-referential imports (`@bloxwap/hyperliquid/...`) in tests, docs and JSDoc examples resolve without a build. The publishable package is emitted by `bun run build`, which writes dist/ with its own package.json whose exports point at .js/.d.ts.", - "//exports:narrow": "The `.` barrel re-exports all four clients plus the transports, so importing it evaluates the exchange and signing graph even for a read-only process: 25.2 ms on Node, 16.5 ms on Bun (published bundle), against 17.7 / 5.5 ms for an info-only consumer entering through `./api/info/client` + `./transport`, and 6.5 / 4.6 ms through `./api/info/client` + `./transport/http`. The `./transport*` and `./api/*/client` keys below exist for that consumer \u2014 `.` stays the compatible default and nothing under src/ changed to add them.", + "//exports:narrow": "Use individual ./api// entry points and ./transport/http for small consumers. Client entry points load their own method families; the root barrel includes all clients and transports. Builders are available through ./actions/. Published exports use compact patterns, with private underscore paths blocked, to avoid a large package manifest slowing cold imports.", "exports": { ".": "./src/mod.ts", "./signing": "./src/signing/mod.ts", @@ -47,7 +47,267 @@ "./api/info": "./src/api/info/mod.ts", "./api/info/client": "./src/api/info/client.ts", "./api/subscription": "./src/api/subscription/mod.ts", - "./api/subscription/client": "./src/api/subscription/client.ts" + "./api/subscription/client": "./src/api/subscription/client.ts", + "./actions": "./src/actions/mod.ts", + "./actions/execution": "./src/actions/execution.ts", + "./api/info/activeAssetData": "./src/api/info/_methods/activeAssetData.ts", + "./api/info/allBorrowLendReserveStates": "./src/api/info/_methods/allBorrowLendReserveStates.ts", + "./api/info/allMids": "./src/api/info/_methods/allMids.ts", + "./api/info/allPerpMetas": "./src/api/info/_methods/allPerpMetas.ts", + "./api/info/approvedBuilders": "./src/api/info/_methods/approvedBuilders.ts", + "./api/info/borrowLendReserveState": "./src/api/info/_methods/borrowLendReserveState.ts", + "./api/info/borrowLendUserState": "./src/api/info/_methods/borrowLendUserState.ts", + "./api/info/candleSnapshot": "./src/api/info/_methods/candleSnapshot.ts", + "./api/info/candleSnapshotAll": "./src/api/info/_methods/candleSnapshotAll.ts", + "./api/info/candleSnapshotPages": "./src/api/info/_methods/candleSnapshotPages.ts", + "./api/info/clearinghouseState": "./src/api/info/_methods/clearinghouseState.ts", + "./api/info/delegations": "./src/api/info/_methods/delegations.ts", + "./api/info/delegatorHistory": "./src/api/info/_methods/delegatorHistory.ts", + "./api/info/delegatorRewards": "./src/api/info/_methods/delegatorRewards.ts", + "./api/info/delegatorSummary": "./src/api/info/_methods/delegatorSummary.ts", + "./api/info/exchangeStatus": "./src/api/info/_methods/exchangeStatus.ts", + "./api/info/extraAgents": "./src/api/info/_methods/extraAgents.ts", + "./api/info/frontendOpenOrders": "./src/api/info/_methods/frontendOpenOrders.ts", + "./api/info/fundingHistory": "./src/api/info/_methods/fundingHistory.ts", + "./api/info/fundingHistoryAll": "./src/api/info/_methods/fundingHistoryAll.ts", + "./api/info/fundingHistoryPages": "./src/api/info/_methods/fundingHistoryPages.ts", + "./api/info/gossipPriorityAuctionStatus": "./src/api/info/_methods/gossipPriorityAuctionStatus.ts", + "./api/info/gossipRootIps": "./src/api/info/_methods/gossipRootIps.ts", + "./api/info/historicalOrders": "./src/api/info/_methods/historicalOrders.ts", + "./api/info/isVip": "./src/api/info/_methods/isVip.ts", + "./api/info/l2Book": "./src/api/info/_methods/l2Book.ts", + "./api/info/leadingVaults": "./src/api/info/_methods/leadingVaults.ts", + "./api/info/legalCheck": "./src/api/info/_methods/legalCheck.ts", + "./api/info/liquidatable": "./src/api/info/_methods/liquidatable.ts", + "./api/info/marginTable": "./src/api/info/_methods/marginTable.ts", + "./api/info/maxBuilderFee": "./src/api/info/_methods/maxBuilderFee.ts", + "./api/info/maxMarketOrderNtls": "./src/api/info/_methods/maxMarketOrderNtls.ts", + "./api/info/meta": "./src/api/info/_methods/meta.ts", + "./api/info/metaAndAssetCtxs": "./src/api/info/_methods/metaAndAssetCtxs.ts", + "./api/info/openOrders": "./src/api/info/_methods/openOrders.ts", + "./api/info/orderStatus": "./src/api/info/_methods/orderStatus.ts", + "./api/info/outcomeDeployerLimits": "./src/api/info/_methods/outcomeDeployerLimits.ts", + "./api/info/outcomeMeta": "./src/api/info/_methods/outcomeMeta.ts", + "./api/info/outcomeTemplates": "./src/api/info/_methods/outcomeTemplates.ts", + "./api/info/perpAnnotation": "./src/api/info/_methods/perpAnnotation.ts", + "./api/info/perpCategories": "./src/api/info/_methods/perpCategories.ts", + "./api/info/perpConciseAnnotations": "./src/api/info/_methods/perpConciseAnnotations.ts", + "./api/info/perpDeployAuctionStatus": "./src/api/info/_methods/perpDeployAuctionStatus.ts", + "./api/info/perpDexLimits": "./src/api/info/_methods/perpDexLimits.ts", + "./api/info/perpDexStatus": "./src/api/info/_methods/perpDexStatus.ts", + "./api/info/perpDexes": "./src/api/info/_methods/perpDexes.ts", + "./api/info/perpDexs": "./src/api/info/_methods/perpDexs.ts", + "./api/info/perpsAtOpenInterestCap": "./src/api/info/_methods/perpsAtOpenInterestCap.ts", + "./api/info/portfolio": "./src/api/info/_methods/portfolio.ts", + "./api/info/preTransferCheck": "./src/api/info/_methods/preTransferCheck.ts", + "./api/info/predictedFundings": "./src/api/info/_methods/predictedFundings.ts", + "./api/info/recentTrades": "./src/api/info/_methods/recentTrades.ts", + "./api/info/referral": "./src/api/info/_methods/referral.ts", + "./api/info/settledOutcome": "./src/api/info/_methods/settledOutcome.ts", + "./api/info/spotClearinghouseState": "./src/api/info/_methods/spotClearinghouseState.ts", + "./api/info/spotDeployState": "./src/api/info/_methods/spotDeployState.ts", + "./api/info/spotMeta": "./src/api/info/_methods/spotMeta.ts", + "./api/info/spotMetaAndAssetCtxs": "./src/api/info/_methods/spotMetaAndAssetCtxs.ts", + "./api/info/spotPairDeployAuctionStatus": "./src/api/info/_methods/spotPairDeployAuctionStatus.ts", + "./api/info/subAccounts": "./src/api/info/_methods/subAccounts.ts", + "./api/info/subAccounts2": "./src/api/info/_methods/subAccounts2.ts", + "./api/info/subAccountsV2": "./src/api/info/_methods/subAccountsV2.ts", + "./api/info/tokenDetails": "./src/api/info/_methods/tokenDetails.ts", + "./api/info/twapHistory": "./src/api/info/_methods/twapHistory.ts", + "./api/info/usdcRouting": "./src/api/info/_methods/usdcRouting.ts", + "./api/info/userAbstraction": "./src/api/info/_methods/userAbstraction.ts", + "./api/info/userBorrowLendInterest": "./src/api/info/_methods/userBorrowLendInterest.ts", + "./api/info/userDexAbstraction": "./src/api/info/_methods/userDexAbstraction.ts", + "./api/info/userFees": "./src/api/info/_methods/userFees.ts", + "./api/info/userFills": "./src/api/info/_methods/userFills.ts", + "./api/info/userFillsByTime": "./src/api/info/_methods/userFillsByTime.ts", + "./api/info/userFillsByTimeAll": "./src/api/info/_methods/userFillsByTimeAll.ts", + "./api/info/userFillsByTimePages": "./src/api/info/_methods/userFillsByTimePages.ts", + "./api/info/userFunding": "./src/api/info/_methods/userFunding.ts", + "./api/info/userNonFundingLedgerUpdates": "./src/api/info/_methods/userNonFundingLedgerUpdates.ts", + "./api/info/userNonFundingLedgerUpdatesAll": "./src/api/info/_methods/userNonFundingLedgerUpdatesAll.ts", + "./api/info/userNonFundingLedgerUpdatesPages": "./src/api/info/_methods/userNonFundingLedgerUpdatesPages.ts", + "./api/info/userRateLimit": "./src/api/info/_methods/userRateLimit.ts", + "./api/info/userRole": "./src/api/info/_methods/userRole.ts", + "./api/info/userStarState": "./src/api/info/_methods/userStarState.ts", + "./api/info/userToMultiSigSigners": "./src/api/info/_methods/userToMultiSigSigners.ts", + "./api/info/userTwapSliceFills": "./src/api/info/_methods/userTwapSliceFills.ts", + "./api/info/userTwapSliceFillsByTime": "./src/api/info/_methods/userTwapSliceFillsByTime.ts", + "./api/info/userTwapSliceFillsByTimeAll": "./src/api/info/_methods/userTwapSliceFillsByTimeAll.ts", + "./api/info/userTwapSliceFillsByTimePages": "./src/api/info/_methods/userTwapSliceFillsByTimePages.ts", + "./api/info/userVaultEquities": "./src/api/info/_methods/userVaultEquities.ts", + "./api/info/validatorL1Votes": "./src/api/info/_methods/validatorL1Votes.ts", + "./api/info/validatorSummaries": "./src/api/info/_methods/validatorSummaries.ts", + "./api/info/vaultDetails": "./src/api/info/_methods/vaultDetails.ts", + "./api/info/vaultSummaries": "./src/api/info/_methods/vaultSummaries.ts", + "./api/info/webData2": "./src/api/info/_methods/webData2.ts", + "./api/exchange/activateOutcomeDeployer": "./src/api/exchange/_methods/activateOutcomeDeployer.ts", + "./api/exchange/agentEnableDexAbstraction": "./src/api/exchange/_methods/agentEnableDexAbstraction.ts", + "./api/exchange/agentSendAsset": "./src/api/exchange/_methods/agentSendAsset.ts", + "./api/exchange/agentSetAbstraction": "./src/api/exchange/_methods/agentSetAbstraction.ts", + "./api/exchange/approveAgent": "./src/api/exchange/_methods/approveAgent.ts", + "./api/exchange/approveBuilderFee": "./src/api/exchange/_methods/approveBuilderFee.ts", + "./api/exchange/authorizeAqav2Role": "./src/api/exchange/_methods/authorizeAqav2Role.ts", + "./api/exchange/batchModify": "./src/api/exchange/_methods/batchModify.ts", + "./api/exchange/borrowLend": "./src/api/exchange/_methods/borrowLend.ts", + "./api/exchange/cDeposit": "./src/api/exchange/_methods/cDeposit.ts", + "./api/exchange/cSignerAction": "./src/api/exchange/_methods/cSignerAction.ts", + "./api/exchange/cValidatorAction": "./src/api/exchange/_methods/cValidatorAction.ts", + "./api/exchange/cWithdraw": "./src/api/exchange/_methods/cWithdraw.ts", + "./api/exchange/cancel": "./src/api/exchange/_methods/cancel.ts", + "./api/exchange/cancelByCloid": "./src/api/exchange/_methods/cancelByCloid.ts", + "./api/exchange/claimRewards": "./src/api/exchange/_methods/claimRewards.ts", + "./api/exchange/convertToMultiSigUser": "./src/api/exchange/_methods/convertToMultiSigUser.ts", + "./api/exchange/createSubAccount": "./src/api/exchange/_methods/createSubAccount.ts", + "./api/exchange/createVault": "./src/api/exchange/_methods/createVault.ts", + "./api/exchange/evmUserModify": "./src/api/exchange/_methods/evmUserModify.ts", + "./api/exchange/finalizeEvmContract": "./src/api/exchange/_methods/finalizeEvmContract.ts", + "./api/exchange/gossipPriorityBid": "./src/api/exchange/_methods/gossipPriorityBid.ts", + "./api/exchange/hip3LiquidatorTransfer": "./src/api/exchange/_methods/hip3LiquidatorTransfer.ts", + "./api/exchange/linkStakingUser": "./src/api/exchange/_methods/linkStakingUser.ts", + "./api/exchange/modify": "./src/api/exchange/_methods/modify.ts", + "./api/exchange/noop": "./src/api/exchange/_methods/noop.ts", + "./api/exchange/order": "./src/api/exchange/_methods/order.ts", + "./api/exchange/outcomeDeploy": "./src/api/exchange/_methods/outcomeDeploy.ts", + "./api/exchange/perpDeploy": "./src/api/exchange/_methods/perpDeploy.ts", + "./api/exchange/prepareRequest": "./src/api/exchange/_methods/prepareRequest.ts", + "./api/exchange/registerReferrer": "./src/api/exchange/_methods/registerReferrer.ts", + "./api/exchange/reserveRequestWeight": "./src/api/exchange/_methods/reserveRequestWeight.ts", + "./api/exchange/scheduleCancel": "./src/api/exchange/_methods/scheduleCancel.ts", + "./api/exchange/sendAsset": "./src/api/exchange/_methods/sendAsset.ts", + "./api/exchange/sendToEvmWithData": "./src/api/exchange/_methods/sendToEvmWithData.ts", + "./api/exchange/setDisplayName": "./src/api/exchange/_methods/setDisplayName.ts", + "./api/exchange/setReferrer": "./src/api/exchange/_methods/setReferrer.ts", + "./api/exchange/spotDeploy": "./src/api/exchange/_methods/spotDeploy.ts", + "./api/exchange/spotSend": "./src/api/exchange/_methods/spotSend.ts", + "./api/exchange/spotUser": "./src/api/exchange/_methods/spotUser.ts", + "./api/exchange/stakingDeposit": "./src/api/exchange/_methods/stakingDeposit.ts", + "./api/exchange/stakingLinkDisableTradingUser": "./src/api/exchange/_methods/stakingLinkDisableTradingUser.ts", + "./api/exchange/stakingWithdraw": "./src/api/exchange/_methods/stakingWithdraw.ts", + "./api/exchange/subAccountModify": "./src/api/exchange/_methods/subAccountModify.ts", + "./api/exchange/subAccountSpotTransfer": "./src/api/exchange/_methods/subAccountSpotTransfer.ts", + "./api/exchange/subAccountTransfer": "./src/api/exchange/_methods/subAccountTransfer.ts", + "./api/exchange/submitPrepared": "./src/api/exchange/_methods/submitPrepared.ts", + "./api/exchange/tokenDelegate": "./src/api/exchange/_methods/tokenDelegate.ts", + "./api/exchange/topUpIsolatedOnlyMargin": "./src/api/exchange/_methods/topUpIsolatedOnlyMargin.ts", + "./api/exchange/twapCancel": "./src/api/exchange/_methods/twapCancel.ts", + "./api/exchange/twapOrder": "./src/api/exchange/_methods/twapOrder.ts", + "./api/exchange/updateIsolatedMargin": "./src/api/exchange/_methods/updateIsolatedMargin.ts", + "./api/exchange/updateLeverage": "./src/api/exchange/_methods/updateLeverage.ts", + "./api/exchange/usdClassTransfer": "./src/api/exchange/_methods/usdClassTransfer.ts", + "./api/exchange/usdSend": "./src/api/exchange/_methods/usdSend.ts", + "./api/exchange/userDexAbstraction": "./src/api/exchange/_methods/userDexAbstraction.ts", + "./api/exchange/userOutcome": "./src/api/exchange/_methods/userOutcome.ts", + "./api/exchange/userPortfolioMargin": "./src/api/exchange/_methods/userPortfolioMargin.ts", + "./api/exchange/userSetAbstraction": "./src/api/exchange/_methods/userSetAbstraction.ts", + "./api/exchange/validatorAction": "./src/api/exchange/_methods/validatorAction.ts", + "./api/exchange/validatorL1Stream": "./src/api/exchange/_methods/validatorL1Stream.ts", + "./api/exchange/validatorSignerAction": "./src/api/exchange/_methods/validatorSignerAction.ts", + "./api/exchange/vaultDistribute": "./src/api/exchange/_methods/vaultDistribute.ts", + "./api/exchange/vaultModify": "./src/api/exchange/_methods/vaultModify.ts", + "./api/exchange/vaultTransfer": "./src/api/exchange/_methods/vaultTransfer.ts", + "./api/exchange/warmup": "./src/api/exchange/_methods/warmup.ts", + "./api/exchange/withdraw": "./src/api/exchange/_methods/withdraw.ts", + "./api/exchange/withdraw3": "./src/api/exchange/_methods/withdraw3.ts", + "./api/explorer/blockDetails": "./src/api/explorer/_methods/blockDetails.ts", + "./api/explorer/explorerBlock": "./src/api/explorer/_methods/explorerBlock.ts", + "./api/explorer/explorerTxs": "./src/api/explorer/_methods/explorerTxs.ts", + "./api/explorer/txDetails": "./src/api/explorer/_methods/txDetails.ts", + "./api/explorer/userDetails": "./src/api/explorer/_methods/userDetails.ts", + "./api/subscription/activeAssetCtx": "./src/api/subscription/_methods/activeAssetCtx.ts", + "./api/subscription/activeAssetData": "./src/api/subscription/_methods/activeAssetData.ts", + "./api/subscription/activeSpotAssetCtx": "./src/api/subscription/_methods/activeSpotAssetCtx.ts", + "./api/subscription/allDexsAssetCtxs": "./src/api/subscription/_methods/allDexsAssetCtxs.ts", + "./api/subscription/allDexsClearinghouseState": "./src/api/subscription/_methods/allDexsClearinghouseState.ts", + "./api/subscription/allMids": "./src/api/subscription/_methods/allMids.ts", + "./api/subscription/assetCtxs": "./src/api/subscription/_methods/assetCtxs.ts", + "./api/subscription/bbo": "./src/api/subscription/_methods/bbo.ts", + "./api/subscription/candle": "./src/api/subscription/_methods/candle.ts", + "./api/subscription/clearinghouseState": "./src/api/subscription/_methods/clearinghouseState.ts", + "./api/subscription/fastAssetCtxs": "./src/api/subscription/_methods/fastAssetCtxs.ts", + "./api/subscription/l2Book": "./src/api/subscription/_methods/l2Book.ts", + "./api/subscription/notification": "./src/api/subscription/_methods/notification.ts", + "./api/subscription/openOrders": "./src/api/subscription/_methods/openOrders.ts", + "./api/subscription/orderUpdates": "./src/api/subscription/_methods/orderUpdates.ts", + "./api/subscription/outcomeMetaUpdates": "./src/api/subscription/_methods/outcomeMetaUpdates.ts", + "./api/subscription/spotAssetCtxs": "./src/api/subscription/_methods/spotAssetCtxs.ts", + "./api/subscription/spotState": "./src/api/subscription/_methods/spotState.ts", + "./api/subscription/trades": "./src/api/subscription/_methods/trades.ts", + "./api/subscription/twapStates": "./src/api/subscription/_methods/twapStates.ts", + "./api/subscription/unsubscribeAll": "./src/api/subscription/_methods/unsubscribeAll.ts", + "./api/subscription/userEvents": "./src/api/subscription/_methods/userEvents.ts", + "./api/subscription/userFills": "./src/api/subscription/_methods/userFills.ts", + "./api/subscription/userFundings": "./src/api/subscription/_methods/userFundings.ts", + "./api/subscription/userHistoricalOrders": "./src/api/subscription/_methods/userHistoricalOrders.ts", + "./api/subscription/userNonFundingLedgerUpdates": "./src/api/subscription/_methods/userNonFundingLedgerUpdates.ts", + "./api/subscription/userTwapHistory": "./src/api/subscription/_methods/userTwapHistory.ts", + "./api/subscription/userTwapSliceFills": "./src/api/subscription/_methods/userTwapSliceFills.ts", + "./api/subscription/webData3": "./src/api/subscription/_methods/webData3.ts", + "./actions/activateOutcomeDeployer": "./src/actions/activateOutcomeDeployer.ts", + "./actions/agentEnableDexAbstraction": "./src/actions/agentEnableDexAbstraction.ts", + "./actions/agentSendAsset": "./src/actions/agentSendAsset.ts", + "./actions/agentSetAbstraction": "./src/actions/agentSetAbstraction.ts", + "./actions/approveAgent": "./src/actions/approveAgent.ts", + "./actions/approveBuilderFee": "./src/actions/approveBuilderFee.ts", + "./actions/authorizeAqav2Role": "./src/actions/authorizeAqav2Role.ts", + "./actions/batchModify": "./src/actions/batchModify.ts", + "./actions/borrowLend": "./src/actions/borrowLend.ts", + "./actions/cDeposit": "./src/actions/cDeposit.ts", + "./actions/cSignerAction": "./src/actions/cSignerAction.ts", + "./actions/cValidatorAction": "./src/actions/cValidatorAction.ts", + "./actions/cWithdraw": "./src/actions/cWithdraw.ts", + "./actions/cancel": "./src/actions/cancel.ts", + "./actions/cancelByCloid": "./src/actions/cancelByCloid.ts", + "./actions/claimRewards": "./src/actions/claimRewards.ts", + "./actions/convertToMultiSigUser": "./src/actions/convertToMultiSigUser.ts", + "./actions/createSubAccount": "./src/actions/createSubAccount.ts", + "./actions/createVault": "./src/actions/createVault.ts", + "./actions/evmUserModify": "./src/actions/evmUserModify.ts", + "./actions/finalizeEvmContract": "./src/actions/finalizeEvmContract.ts", + "./actions/gossipPriorityBid": "./src/actions/gossipPriorityBid.ts", + "./actions/hip3LiquidatorTransfer": "./src/actions/hip3LiquidatorTransfer.ts", + "./actions/linkStakingUser": "./src/actions/linkStakingUser.ts", + "./actions/modify": "./src/actions/modify.ts", + "./actions/noop": "./src/actions/noop.ts", + "./actions/order": "./src/actions/order.ts", + "./actions/outcomeDeploy": "./src/actions/outcomeDeploy.ts", + "./actions/perpDeploy": "./src/actions/perpDeploy.ts", + "./actions/registerReferrer": "./src/actions/registerReferrer.ts", + "./actions/reserveRequestWeight": "./src/actions/reserveRequestWeight.ts", + "./actions/scheduleCancel": "./src/actions/scheduleCancel.ts", + "./actions/sendAsset": "./src/actions/sendAsset.ts", + "./actions/sendToEvmWithData": "./src/actions/sendToEvmWithData.ts", + "./actions/setDisplayName": "./src/actions/setDisplayName.ts", + "./actions/setReferrer": "./src/actions/setReferrer.ts", + "./actions/spotDeploy": "./src/actions/spotDeploy.ts", + "./actions/spotSend": "./src/actions/spotSend.ts", + "./actions/spotUser": "./src/actions/spotUser.ts", + "./actions/stakingLinkDisableTradingUser": "./src/actions/stakingLinkDisableTradingUser.ts", + "./actions/subAccountModify": "./src/actions/subAccountModify.ts", + "./actions/subAccountSpotTransfer": "./src/actions/subAccountSpotTransfer.ts", + "./actions/subAccountTransfer": "./src/actions/subAccountTransfer.ts", + "./actions/tokenDelegate": "./src/actions/tokenDelegate.ts", + "./actions/topUpIsolatedOnlyMargin": "./src/actions/topUpIsolatedOnlyMargin.ts", + "./actions/twapCancel": "./src/actions/twapCancel.ts", + "./actions/twapOrder": "./src/actions/twapOrder.ts", + "./actions/updateIsolatedMargin": "./src/actions/updateIsolatedMargin.ts", + "./actions/updateLeverage": "./src/actions/updateLeverage.ts", + "./actions/usdClassTransfer": "./src/actions/usdClassTransfer.ts", + "./actions/usdSend": "./src/actions/usdSend.ts", + "./actions/userDexAbstraction": "./src/actions/userDexAbstraction.ts", + "./actions/userOutcome": "./src/actions/userOutcome.ts", + "./actions/userPortfolioMargin": "./src/actions/userPortfolioMargin.ts", + "./actions/userSetAbstraction": "./src/actions/userSetAbstraction.ts", + "./actions/validatorL1Stream": "./src/actions/validatorL1Stream.ts", + "./actions/vaultDistribute": "./src/actions/vaultDistribute.ts", + "./actions/vaultModify": "./src/actions/vaultModify.ts", + "./actions/vaultTransfer": "./src/actions/vaultTransfer.ts", + "./actions/withdraw3": "./src/actions/withdraw3.ts", + "./actions/stakingDeposit": "./src/actions/stakingDeposit.ts", + "./actions/stakingWithdraw": "./src/actions/stakingWithdraw.ts", + "./actions/validatorAction": "./src/actions/validatorAction.ts", + "./actions/validatorSignerAction": "./src/actions/validatorSignerAction.ts", + "./actions/withdraw": "./src/actions/withdraw.ts" }, "scripts": { "//check": "SDK checks; workspace formatting, linting, and documentation checks run from the root.", diff --git a/packages/hyperliquid/src/actions/_canonical.ts b/packages/hyperliquid/src/actions/_canonical.ts new file mode 100644 index 00000000..a58cb728 --- /dev/null +++ b/packages/hyperliquid/src/actions/_canonical.ts @@ -0,0 +1,51 @@ +/** Canonical action ownership and signing metadata. @module */ +import { HyperliquidError } from "../_base.ts"; +import { registerImmutableL1Action } from "../signing/_l1Cache.ts"; +import { registerImmutableWireAction } from "../transport/_wire.ts"; + +declare const responseType: unique symbol; +/** Validated, owned action. Construct through an operation builder, not a type assertion. */ +export interface CanonicalAction { + /** Canonical wire fields, recursively immutable at runtime. */ + readonly payload: Readonly>; + readonly [responseType]?: T; +} + +/** Metadata kept outside the public action and wire payload. */ +export interface ActionMetadata { + kind: "l1" | "user"; + types?: Record; + nonce?: number; + toMultiSigPayloadAction?: (action: Readonly>) => Record; + toSinglePayloadAction?: (action: Readonly>) => Record; +} +const metadata = new WeakMap(); + +/** Copy before freezing so callers retain ownership of their input; preserve key order. */ +export function immutableCopy(value: T): T { + if (Array.isArray(value)) return Object.freeze(value.map(immutableCopy)) as T; + if (value !== null && typeof value === "object") { + const copy: Record = {}; + for (const [key, child] of Object.entries(value)) { + Object.defineProperty(copy, key, { value: immutableCopy(child), enumerable: true }); + } + return Object.freeze(copy) as T; + } + return value; +} + +/** Internal factory used by validated operation builders. */ +export function canonicalAction(payload: Record, info: ActionMetadata): CanonicalAction { + const action = Object.freeze({ payload: immutableCopy(payload) }); + if (info.kind === "l1") registerImmutableL1Action(action.payload); + registerImmutableWireAction(action.payload); + metadata.set(action, { ...info, types: info.types ? immutableCopy(info.types) : undefined }); + return action; +} + +/** Reject foreign or reconstructed objects; serialization does not preserve validation ownership. */ +export function actionMetadata(action: CanonicalAction): ActionMetadata { + const info = metadata.get(action); + if (!info) throw new HyperliquidError("Expected an action produced by an SDK builder"); + return info; +} diff --git a/packages/hyperliquid/src/actions/activateOutcomeDeployer.ts b/packages/hyperliquid/src/actions/activateOutcomeDeployer.ts new file mode 100644 index 00000000..44e8ae2c --- /dev/null +++ b/packages/hyperliquid/src/actions/activateOutcomeDeployer.ts @@ -0,0 +1,7 @@ +/** Canonical activateOutcomeDeployer action builder. @module */ +export { buildActivateOutcomeDeployer } from "../api/exchange/_methods/activateOutcomeDeployer.ts"; +export type { + ActivateOutcomeDeployerOptions, + ActivateOutcomeDeployerSuccessResponse, + ActivateOutcomeDeployerParameters, +} from "../api/exchange/_methods/activateOutcomeDeployer.ts"; diff --git a/packages/hyperliquid/src/actions/agentEnableDexAbstraction.ts b/packages/hyperliquid/src/actions/agentEnableDexAbstraction.ts new file mode 100644 index 00000000..a62f49a9 --- /dev/null +++ b/packages/hyperliquid/src/actions/agentEnableDexAbstraction.ts @@ -0,0 +1,6 @@ +/** Canonical agentEnableDexAbstraction action builder. @module */ +export { buildAgentEnableDexAbstraction } from "../api/exchange/_methods/agentEnableDexAbstraction.ts"; +export type { + AgentEnableDexAbstractionOptions, + AgentEnableDexAbstractionSuccessResponse, +} from "../api/exchange/_methods/agentEnableDexAbstraction.ts"; diff --git a/packages/hyperliquid/src/actions/agentSendAsset.ts b/packages/hyperliquid/src/actions/agentSendAsset.ts new file mode 100644 index 00000000..1014062e --- /dev/null +++ b/packages/hyperliquid/src/actions/agentSendAsset.ts @@ -0,0 +1,7 @@ +/** Canonical agentSendAsset action builder. @module */ +export { buildAgentSendAsset } from "../api/exchange/_methods/agentSendAsset.ts"; +export type { + AgentSendAssetOptions, + AgentSendAssetSuccessResponse, + AgentSendAssetParameters, +} from "../api/exchange/_methods/agentSendAsset.ts"; diff --git a/packages/hyperliquid/src/actions/agentSetAbstraction.ts b/packages/hyperliquid/src/actions/agentSetAbstraction.ts new file mode 100644 index 00000000..fcc4e474 --- /dev/null +++ b/packages/hyperliquid/src/actions/agentSetAbstraction.ts @@ -0,0 +1,7 @@ +/** Canonical agentSetAbstraction action builder. @module */ +export { buildAgentSetAbstraction } from "../api/exchange/_methods/agentSetAbstraction.ts"; +export type { + AgentSetAbstractionOptions, + AgentSetAbstractionSuccessResponse, + AgentSetAbstractionParameters, +} from "../api/exchange/_methods/agentSetAbstraction.ts"; diff --git a/packages/hyperliquid/src/actions/approveAgent.ts b/packages/hyperliquid/src/actions/approveAgent.ts new file mode 100644 index 00000000..d1568b38 --- /dev/null +++ b/packages/hyperliquid/src/actions/approveAgent.ts @@ -0,0 +1,7 @@ +/** Canonical approveAgent action builder. @module */ +export { buildApproveAgent } from "../api/exchange/_methods/approveAgent.ts"; +export type { + ApproveAgentOptions, + ApproveAgentSuccessResponse, + ApproveAgentParameters, +} from "../api/exchange/_methods/approveAgent.ts"; diff --git a/packages/hyperliquid/src/actions/approveBuilderFee.ts b/packages/hyperliquid/src/actions/approveBuilderFee.ts new file mode 100644 index 00000000..61ef8bf2 --- /dev/null +++ b/packages/hyperliquid/src/actions/approveBuilderFee.ts @@ -0,0 +1,7 @@ +/** Canonical approveBuilderFee action builder. @module */ +export { buildApproveBuilderFee } from "../api/exchange/_methods/approveBuilderFee.ts"; +export type { + ApproveBuilderFeeOptions, + ApproveBuilderFeeSuccessResponse, + ApproveBuilderFeeParameters, +} from "../api/exchange/_methods/approveBuilderFee.ts"; diff --git a/packages/hyperliquid/src/actions/authorizeAqav2Role.ts b/packages/hyperliquid/src/actions/authorizeAqav2Role.ts new file mode 100644 index 00000000..2f721522 --- /dev/null +++ b/packages/hyperliquid/src/actions/authorizeAqav2Role.ts @@ -0,0 +1,7 @@ +/** Canonical authorizeAqav2Role action builder. @module */ +export { buildAuthorizeAqav2Role } from "../api/exchange/_methods/authorizeAqav2Role.ts"; +export type { + AuthorizeAqav2RoleOptions, + AuthorizeAqav2RoleSuccessResponse, + AuthorizeAqav2RoleParameters, +} from "../api/exchange/_methods/authorizeAqav2Role.ts"; diff --git a/packages/hyperliquid/src/actions/batchModify.ts b/packages/hyperliquid/src/actions/batchModify.ts new file mode 100644 index 00000000..689c491b --- /dev/null +++ b/packages/hyperliquid/src/actions/batchModify.ts @@ -0,0 +1,7 @@ +/** Canonical batchModify action builder. @module */ +export { buildBatchModify } from "../api/exchange/_methods/batchModify.ts"; +export type { + BatchModifyOptions, + BatchModifySuccessResponse, + BatchModifyParameters, +} from "../api/exchange/_methods/batchModify.ts"; diff --git a/packages/hyperliquid/src/actions/borrowLend.ts b/packages/hyperliquid/src/actions/borrowLend.ts new file mode 100644 index 00000000..33489842 --- /dev/null +++ b/packages/hyperliquid/src/actions/borrowLend.ts @@ -0,0 +1,7 @@ +/** Canonical borrowLend action builder. @module */ +export { buildBorrowLend } from "../api/exchange/_methods/borrowLend.ts"; +export type { + BorrowLendOptions, + BorrowLendSuccessResponse, + BorrowLendParameters, +} from "../api/exchange/_methods/borrowLend.ts"; diff --git a/packages/hyperliquid/src/actions/cDeposit.ts b/packages/hyperliquid/src/actions/cDeposit.ts new file mode 100644 index 00000000..135b1cfe --- /dev/null +++ b/packages/hyperliquid/src/actions/cDeposit.ts @@ -0,0 +1,7 @@ +/** Canonical cDeposit action builder. @module */ +export { buildCDeposit } from "../api/exchange/_methods/cDeposit.ts"; +export type { + CDepositOptions, + CDepositSuccessResponse, + CDepositParameters, +} from "../api/exchange/_methods/cDeposit.ts"; diff --git a/packages/hyperliquid/src/actions/cSignerAction.ts b/packages/hyperliquid/src/actions/cSignerAction.ts new file mode 100644 index 00000000..5288caf9 --- /dev/null +++ b/packages/hyperliquid/src/actions/cSignerAction.ts @@ -0,0 +1,7 @@ +/** Canonical cSignerAction action builder. @module */ +export { buildCSignerAction } from "../api/exchange/_methods/cSignerAction.ts"; +export type { + CSignerActionOptions, + CSignerActionSuccessResponse, + CSignerActionParameters, +} from "../api/exchange/_methods/cSignerAction.ts"; diff --git a/packages/hyperliquid/src/actions/cValidatorAction.ts b/packages/hyperliquid/src/actions/cValidatorAction.ts new file mode 100644 index 00000000..a871776e --- /dev/null +++ b/packages/hyperliquid/src/actions/cValidatorAction.ts @@ -0,0 +1,7 @@ +/** Canonical cValidatorAction action builder. @module */ +export { buildCValidatorAction } from "../api/exchange/_methods/cValidatorAction.ts"; +export type { + CValidatorActionOptions, + CValidatorActionSuccessResponse, + CValidatorActionParameters, +} from "../api/exchange/_methods/cValidatorAction.ts"; diff --git a/packages/hyperliquid/src/actions/cWithdraw.ts b/packages/hyperliquid/src/actions/cWithdraw.ts new file mode 100644 index 00000000..097aeab8 --- /dev/null +++ b/packages/hyperliquid/src/actions/cWithdraw.ts @@ -0,0 +1,7 @@ +/** Canonical cWithdraw action builder. @module */ +export { buildCWithdraw } from "../api/exchange/_methods/cWithdraw.ts"; +export type { + CWithdrawOptions, + CWithdrawSuccessResponse, + CWithdrawParameters, +} from "../api/exchange/_methods/cWithdraw.ts"; diff --git a/packages/hyperliquid/src/actions/cancel.ts b/packages/hyperliquid/src/actions/cancel.ts new file mode 100644 index 00000000..e88807b1 --- /dev/null +++ b/packages/hyperliquid/src/actions/cancel.ts @@ -0,0 +1,3 @@ +/** Canonical cancel action builder. @module */ +export { buildCancel } from "../api/exchange/_methods/cancel.ts"; +export type { CancelOptions, CancelSuccessResponse, CancelParameters } from "../api/exchange/_methods/cancel.ts"; diff --git a/packages/hyperliquid/src/actions/cancelByCloid.ts b/packages/hyperliquid/src/actions/cancelByCloid.ts new file mode 100644 index 00000000..12b41f35 --- /dev/null +++ b/packages/hyperliquid/src/actions/cancelByCloid.ts @@ -0,0 +1,7 @@ +/** Canonical cancelByCloid action builder. @module */ +export { buildCancelByCloid } from "../api/exchange/_methods/cancelByCloid.ts"; +export type { + CancelByCloidOptions, + CancelByCloidSuccessResponse, + CancelByCloidParameters, +} from "../api/exchange/_methods/cancelByCloid.ts"; diff --git a/packages/hyperliquid/src/actions/claimRewards.ts b/packages/hyperliquid/src/actions/claimRewards.ts new file mode 100644 index 00000000..45e45106 --- /dev/null +++ b/packages/hyperliquid/src/actions/claimRewards.ts @@ -0,0 +1,3 @@ +/** Canonical claimRewards action builder. @module */ +export { buildClaimRewards } from "../api/exchange/_methods/claimRewards.ts"; +export type { ClaimRewardsOptions, ClaimRewardsSuccessResponse } from "../api/exchange/_methods/claimRewards.ts"; diff --git a/packages/hyperliquid/src/actions/convertToMultiSigUser.ts b/packages/hyperliquid/src/actions/convertToMultiSigUser.ts new file mode 100644 index 00000000..9e0d1216 --- /dev/null +++ b/packages/hyperliquid/src/actions/convertToMultiSigUser.ts @@ -0,0 +1,7 @@ +/** Canonical convertToMultiSigUser action builder. @module */ +export { buildConvertToMultiSigUser } from "../api/exchange/_methods/convertToMultiSigUser.ts"; +export type { + ConvertToMultiSigUserOptions, + ConvertToMultiSigUserSuccessResponse, + ConvertToMultiSigUserParameters, +} from "../api/exchange/_methods/convertToMultiSigUser.ts"; diff --git a/packages/hyperliquid/src/actions/createSubAccount.ts b/packages/hyperliquid/src/actions/createSubAccount.ts new file mode 100644 index 00000000..c048b11a --- /dev/null +++ b/packages/hyperliquid/src/actions/createSubAccount.ts @@ -0,0 +1,7 @@ +/** Canonical createSubAccount action builder. @module */ +export { buildCreateSubAccount } from "../api/exchange/_methods/createSubAccount.ts"; +export type { + CreateSubAccountOptions, + CreateSubAccountSuccessResponse, + CreateSubAccountParameters, +} from "../api/exchange/_methods/createSubAccount.ts"; diff --git a/packages/hyperliquid/src/actions/createVault.ts b/packages/hyperliquid/src/actions/createVault.ts new file mode 100644 index 00000000..e8b5bf31 --- /dev/null +++ b/packages/hyperliquid/src/actions/createVault.ts @@ -0,0 +1,7 @@ +/** Canonical createVault action builder. @module */ +export { buildCreateVault } from "../api/exchange/_methods/createVault.ts"; +export type { + CreateVaultOptions, + CreateVaultSuccessResponse, + CreateVaultParameters, +} from "../api/exchange/_methods/createVault.ts"; diff --git a/packages/hyperliquid/src/actions/evmUserModify.ts b/packages/hyperliquid/src/actions/evmUserModify.ts new file mode 100644 index 00000000..a7d4da71 --- /dev/null +++ b/packages/hyperliquid/src/actions/evmUserModify.ts @@ -0,0 +1,7 @@ +/** Canonical evmUserModify action builder. @module */ +export { buildEvmUserModify } from "../api/exchange/_methods/evmUserModify.ts"; +export type { + EvmUserModifyOptions, + EvmUserModifySuccessResponse, + EvmUserModifyParameters, +} from "../api/exchange/_methods/evmUserModify.ts"; diff --git a/packages/hyperliquid/src/actions/execution.ts b/packages/hyperliquid/src/actions/execution.ts new file mode 100644 index 00000000..01ec9bbd --- /dev/null +++ b/packages/hyperliquid/src/actions/execution.ts @@ -0,0 +1,123 @@ +/** Explicit signing and execution of canonical actions. @module */ +import { HyperliquidError } from "../_base.ts"; +import type { ExchangeConfig } from "../api/exchange/_methods/_base/_config.ts"; +import { executeL1Action, executeUserSignedAction } from "../api/exchange/_methods/_base/execute.ts"; +import { + getSigningContext, + linkSigningContext, + type PreparedExchangeRequest, +} from "../api/exchange/_methods/_base/_shell.ts"; +import { assertSuccessResponse } from "../api/exchange/_methods/_base/errors.ts"; +import { actionMetadata, immutableCopy, type CanonicalAction } from "./_canonical.ts"; +import { registerExchangeWireRequest } from "../transport/_wire.ts"; + +export type { CanonicalAction } from "./_canonical.ts"; +/** Options for signing/executing an already validated action. */ +export interface ActionOptions { + vaultAddress?: string; + expiresAfter?: string | number; + signal?: AbortSignal; +} +/** Immutable signed wire request. Response inference and environment ownership survive in-process. */ +export type SignedAction = Readonly, "signature" | "action">> & { + readonly action: Readonly>; + readonly signature: Readonly["signature"]>; +}; +const signedOwners = new WeakMap(); + +function run( + config: ExchangeConfig, + action: CanonicalAction, + options: ActionOptions | undefined, + prepare: boolean, +): Promise { + const info = actionMetadata(action); + if (info.nonce !== undefined) config = { ...config, nonceManager: (): number => info.nonce! }; + if (info.kind === "l1") return executeL1Action(config, action.payload, options, prepare); + if (options?.vaultAddress !== undefined || options?.expiresAfter !== undefined) { + return Promise.reject(new HyperliquidError("User-signed actions do not support vaultAddress or expiresAfter")); + } + return executeUserSignedAction( + config, + action.payload, + info.types!, + { + signal: options?.signal, + toMultiSigPayloadAction: info.toMultiSigPayloadAction, + toSinglePayloadAction: info.toSinglePayloadAction, + }, + prepare, + ); +} + +/** + * Allocate a nonce and sign a validated action without posting it. + * @param config Exchange configuration. + * @param action Action produced by an SDK builder. + * @param options Signing options and cancellation signal. + * @return An immutable signed request with the action's response type. + * @throws {HyperliquidError} When action ownership is invalid. + */ +export async function signAction( + config: ExchangeConfig, + action: CanonicalAction, + options?: ActionOptions, +): Promise> { + const request = await run>(config, action, options, true); + // The unchanged builder payload is already recursively owned/frozen. Retain its identity + // and serialization caches; the shell's remaining L1 fields are scalar metadata plus the + // fresh signature. Transformed user actions and multi-sig wrappers still need a full copy. + const signed = + request.action === action.payload + ? Object.freeze({ ...request, signature: immutableCopy(request.signature) }) + : immutableCopy(request); + registerExchangeWireRequest(signed, signed.action); + const owner = getSigningContext(request)!; + signedOwners.set(signed, owner); + linkSigningContext(signed, owner); + return signed; +} + +/** + * Submit an owned signed action without signing again. + * @param config Exchange configuration. + * @param signed Signed request produced by signAction. + * @param options Cancellation signal for submission. + * @return The action's response. + * @throws {HyperliquidError} When request ownership, network, is invalid. + */ +export async function submitAction( + config: ExchangeConfig, + signed: SignedAction, + options?: { signal?: AbortSignal }, +): Promise { + const owner = signedOwners.get(signed); + if (!owner || owner.isTestnet !== config.transport.isTestnet) { + throw new HyperliquidError("Signed request ownership or network mismatch; rebuild and sign for this network"); + } + const response = await config.transport.request("exchange", signed, options?.signal); + assertSuccessResponse(response); + return response; +} + +/** + * Execute a reusable validated action through the coordinated signing path. + * @param config Exchange configuration. + * @param action Action produced by an SDK builder. + * @param options Signing options and cancellation signal. + * @return The action's response. + * @throws {HyperliquidError} When action ownership is invalid. + */ +export function executeAction( + config: ExchangeConfig, + action: CanonicalAction, + options?: ActionOptions, +): Promise { + try { + return run(config, action, options, false); + } catch (error) { + return Promise.reject(error); + } +} + +export { createNonceManager, type NonceManager } from "../api/exchange/_methods/_base/_nonce.ts"; diff --git a/packages/hyperliquid/src/actions/finalizeEvmContract.ts b/packages/hyperliquid/src/actions/finalizeEvmContract.ts new file mode 100644 index 00000000..5670b347 --- /dev/null +++ b/packages/hyperliquid/src/actions/finalizeEvmContract.ts @@ -0,0 +1,7 @@ +/** Canonical finalizeEvmContract action builder. @module */ +export { buildFinalizeEvmContract } from "../api/exchange/_methods/finalizeEvmContract.ts"; +export type { + FinalizeEvmContractOptions, + FinalizeEvmContractSuccessResponse, + FinalizeEvmContractParameters, +} from "../api/exchange/_methods/finalizeEvmContract.ts"; diff --git a/packages/hyperliquid/src/actions/gossipPriorityBid.ts b/packages/hyperliquid/src/actions/gossipPriorityBid.ts new file mode 100644 index 00000000..d3660585 --- /dev/null +++ b/packages/hyperliquid/src/actions/gossipPriorityBid.ts @@ -0,0 +1,7 @@ +/** Canonical gossipPriorityBid action builder. @module */ +export { buildGossipPriorityBid } from "../api/exchange/_methods/gossipPriorityBid.ts"; +export type { + GossipPriorityBidOptions, + GossipPriorityBidSuccessResponse, + GossipPriorityBidParameters, +} from "../api/exchange/_methods/gossipPriorityBid.ts"; diff --git a/packages/hyperliquid/src/actions/hip3LiquidatorTransfer.ts b/packages/hyperliquid/src/actions/hip3LiquidatorTransfer.ts new file mode 100644 index 00000000..2f393e47 --- /dev/null +++ b/packages/hyperliquid/src/actions/hip3LiquidatorTransfer.ts @@ -0,0 +1,7 @@ +/** Canonical hip3LiquidatorTransfer action builder. @module */ +export { buildHip3LiquidatorTransfer } from "../api/exchange/_methods/hip3LiquidatorTransfer.ts"; +export type { + Hip3LiquidatorTransferOptions, + Hip3LiquidatorTransferSuccessResponse, + Hip3LiquidatorTransferParameters, +} from "../api/exchange/_methods/hip3LiquidatorTransfer.ts"; diff --git a/packages/hyperliquid/src/actions/linkStakingUser.ts b/packages/hyperliquid/src/actions/linkStakingUser.ts new file mode 100644 index 00000000..6a035a2f --- /dev/null +++ b/packages/hyperliquid/src/actions/linkStakingUser.ts @@ -0,0 +1,7 @@ +/** Canonical linkStakingUser action builder. @module */ +export { buildLinkStakingUser } from "../api/exchange/_methods/linkStakingUser.ts"; +export type { + LinkStakingUserOptions, + LinkStakingUserSuccessResponse, + LinkStakingUserParameters, +} from "../api/exchange/_methods/linkStakingUser.ts"; diff --git a/packages/hyperliquid/src/actions/mod.ts b/packages/hyperliquid/src/actions/mod.ts new file mode 100644 index 00000000..99d8a828 --- /dev/null +++ b/packages/hyperliquid/src/actions/mod.ts @@ -0,0 +1,68 @@ +/** Canonical exchange actions and their explicit execution stages. @module */ +export * from "./execution.ts"; +export * from "./activateOutcomeDeployer.ts"; +export * from "./agentEnableDexAbstraction.ts"; +export * from "./agentSendAsset.ts"; +export * from "./agentSetAbstraction.ts"; +export * from "./approveAgent.ts"; +export * from "./approveBuilderFee.ts"; +export * from "./authorizeAqav2Role.ts"; +export * from "./batchModify.ts"; +export * from "./borrowLend.ts"; +export * from "./cDeposit.ts"; +export * from "./cSignerAction.ts"; +export * from "./cValidatorAction.ts"; +export * from "./cWithdraw.ts"; +export * from "./cancel.ts"; +export * from "./cancelByCloid.ts"; +export * from "./claimRewards.ts"; +export * from "./convertToMultiSigUser.ts"; +export * from "./createSubAccount.ts"; +export * from "./createVault.ts"; +export * from "./evmUserModify.ts"; +export * from "./finalizeEvmContract.ts"; +export * from "./gossipPriorityBid.ts"; +export * from "./hip3LiquidatorTransfer.ts"; +export * from "./linkStakingUser.ts"; +export * from "./modify.ts"; +export * from "./noop.ts"; +export * from "./order.ts"; +export * from "./outcomeDeploy.ts"; +export * from "./perpDeploy.ts"; +export * from "./registerReferrer.ts"; +export * from "./reserveRequestWeight.ts"; +export * from "./scheduleCancel.ts"; +export * from "./sendAsset.ts"; +export * from "./sendToEvmWithData.ts"; +export * from "./setDisplayName.ts"; +export * from "./setReferrer.ts"; +export * from "./spotDeploy.ts"; +export * from "./spotSend.ts"; +export * from "./spotUser.ts"; +export * from "./stakingLinkDisableTradingUser.ts"; +export * from "./subAccountModify.ts"; +export * from "./subAccountSpotTransfer.ts"; +export * from "./subAccountTransfer.ts"; +export * from "./tokenDelegate.ts"; +export * from "./topUpIsolatedOnlyMargin.ts"; +export * from "./twapCancel.ts"; +export * from "./twapOrder.ts"; +export * from "./updateIsolatedMargin.ts"; +export * from "./updateLeverage.ts"; +export * from "./usdClassTransfer.ts"; +export * from "./usdSend.ts"; +export * from "./userDexAbstraction.ts"; +export * from "./userOutcome.ts"; +export * from "./userPortfolioMargin.ts"; +export * from "./userSetAbstraction.ts"; +export * from "./validatorL1Stream.ts"; +export * from "./vaultDistribute.ts"; +export * from "./vaultModify.ts"; +export * from "./vaultTransfer.ts"; +export * from "./withdraw3.ts"; + +export * from "./stakingDeposit.ts"; +export * from "./stakingWithdraw.ts"; +export * from "./validatorAction.ts"; +export * from "./validatorSignerAction.ts"; +export * from "./withdraw.ts"; diff --git a/packages/hyperliquid/src/actions/modify.ts b/packages/hyperliquid/src/actions/modify.ts new file mode 100644 index 00000000..95a9a230 --- /dev/null +++ b/packages/hyperliquid/src/actions/modify.ts @@ -0,0 +1,3 @@ +/** Canonical modify action builder. @module */ +export { buildModify } from "../api/exchange/_methods/modify.ts"; +export type { ModifyOptions, ModifySuccessResponse, ModifyParameters } from "../api/exchange/_methods/modify.ts"; diff --git a/packages/hyperliquid/src/actions/noop.ts b/packages/hyperliquid/src/actions/noop.ts new file mode 100644 index 00000000..3622ebc0 --- /dev/null +++ b/packages/hyperliquid/src/actions/noop.ts @@ -0,0 +1,3 @@ +/** Canonical noop action builder. @module */ +export { buildNoop } from "../api/exchange/_methods/noop.ts"; +export type { NoopOptions, NoopSuccessResponse, NoopParameters } from "../api/exchange/_methods/noop.ts"; diff --git a/packages/hyperliquid/src/actions/order.ts b/packages/hyperliquid/src/actions/order.ts new file mode 100644 index 00000000..3d5bce4b --- /dev/null +++ b/packages/hyperliquid/src/actions/order.ts @@ -0,0 +1,3 @@ +/** Canonical order action builder. @module */ +export { buildOrder } from "../api/exchange/_methods/order.ts"; +export type { OrderOptions, OrderSuccessResponse, OrderParameters } from "../api/exchange/_methods/order.ts"; diff --git a/packages/hyperliquid/src/actions/outcomeDeploy.ts b/packages/hyperliquid/src/actions/outcomeDeploy.ts new file mode 100644 index 00000000..196463b8 --- /dev/null +++ b/packages/hyperliquid/src/actions/outcomeDeploy.ts @@ -0,0 +1,7 @@ +/** Canonical outcomeDeploy action builder. @module */ +export { buildOutcomeDeploy } from "../api/exchange/_methods/outcomeDeploy.ts"; +export type { + OutcomeDeployOptions, + OutcomeDeploySuccessResponse, + OutcomeDeployParameters, +} from "../api/exchange/_methods/outcomeDeploy.ts"; diff --git a/packages/hyperliquid/src/actions/perpDeploy.ts b/packages/hyperliquid/src/actions/perpDeploy.ts new file mode 100644 index 00000000..769e4b2a --- /dev/null +++ b/packages/hyperliquid/src/actions/perpDeploy.ts @@ -0,0 +1,7 @@ +/** Canonical perpDeploy action builder. @module */ +export { buildPerpDeploy } from "../api/exchange/_methods/perpDeploy.ts"; +export type { + PerpDeployOptions, + PerpDeploySuccessResponse, + PerpDeployParameters, +} from "../api/exchange/_methods/perpDeploy.ts"; diff --git a/packages/hyperliquid/src/actions/registerReferrer.ts b/packages/hyperliquid/src/actions/registerReferrer.ts new file mode 100644 index 00000000..9b8c774b --- /dev/null +++ b/packages/hyperliquid/src/actions/registerReferrer.ts @@ -0,0 +1,7 @@ +/** Canonical registerReferrer action builder. @module */ +export { buildRegisterReferrer } from "../api/exchange/_methods/registerReferrer.ts"; +export type { + RegisterReferrerOptions, + RegisterReferrerSuccessResponse, + RegisterReferrerParameters, +} from "../api/exchange/_methods/registerReferrer.ts"; diff --git a/packages/hyperliquid/src/actions/reserveRequestWeight.ts b/packages/hyperliquid/src/actions/reserveRequestWeight.ts new file mode 100644 index 00000000..69652d18 --- /dev/null +++ b/packages/hyperliquid/src/actions/reserveRequestWeight.ts @@ -0,0 +1,7 @@ +/** Canonical reserveRequestWeight action builder. @module */ +export { buildReserveRequestWeight } from "../api/exchange/_methods/reserveRequestWeight.ts"; +export type { + ReserveRequestWeightOptions, + ReserveRequestWeightSuccessResponse, + ReserveRequestWeightParameters, +} from "../api/exchange/_methods/reserveRequestWeight.ts"; diff --git a/packages/hyperliquid/src/actions/scheduleCancel.ts b/packages/hyperliquid/src/actions/scheduleCancel.ts new file mode 100644 index 00000000..86055b54 --- /dev/null +++ b/packages/hyperliquid/src/actions/scheduleCancel.ts @@ -0,0 +1,7 @@ +/** Canonical scheduleCancel action builder. @module */ +export { buildScheduleCancel } from "../api/exchange/_methods/scheduleCancel.ts"; +export type { + ScheduleCancelOptions, + ScheduleCancelSuccessResponse, + ScheduleCancelParameters, +} from "../api/exchange/_methods/scheduleCancel.ts"; diff --git a/packages/hyperliquid/src/actions/sendAsset.ts b/packages/hyperliquid/src/actions/sendAsset.ts new file mode 100644 index 00000000..197b473c --- /dev/null +++ b/packages/hyperliquid/src/actions/sendAsset.ts @@ -0,0 +1,7 @@ +/** Canonical sendAsset action builder. @module */ +export { buildSendAsset } from "../api/exchange/_methods/sendAsset.ts"; +export type { + SendAssetOptions, + SendAssetSuccessResponse, + SendAssetParameters, +} from "../api/exchange/_methods/sendAsset.ts"; diff --git a/packages/hyperliquid/src/actions/sendToEvmWithData.ts b/packages/hyperliquid/src/actions/sendToEvmWithData.ts new file mode 100644 index 00000000..3d035e94 --- /dev/null +++ b/packages/hyperliquid/src/actions/sendToEvmWithData.ts @@ -0,0 +1,7 @@ +/** Canonical sendToEvmWithData action builder. @module */ +export { buildSendToEvmWithData } from "../api/exchange/_methods/sendToEvmWithData.ts"; +export type { + SendToEvmWithDataOptions, + SendToEvmWithDataSuccessResponse, + SendToEvmWithDataParameters, +} from "../api/exchange/_methods/sendToEvmWithData.ts"; diff --git a/packages/hyperliquid/src/actions/setDisplayName.ts b/packages/hyperliquid/src/actions/setDisplayName.ts new file mode 100644 index 00000000..cc222b6f --- /dev/null +++ b/packages/hyperliquid/src/actions/setDisplayName.ts @@ -0,0 +1,7 @@ +/** Canonical setDisplayName action builder. @module */ +export { buildSetDisplayName } from "../api/exchange/_methods/setDisplayName.ts"; +export type { + SetDisplayNameOptions, + SetDisplayNameSuccessResponse, + SetDisplayNameParameters, +} from "../api/exchange/_methods/setDisplayName.ts"; diff --git a/packages/hyperliquid/src/actions/setReferrer.ts b/packages/hyperliquid/src/actions/setReferrer.ts new file mode 100644 index 00000000..d4ec8329 --- /dev/null +++ b/packages/hyperliquid/src/actions/setReferrer.ts @@ -0,0 +1,7 @@ +/** Canonical setReferrer action builder. @module */ +export { buildSetReferrer } from "../api/exchange/_methods/setReferrer.ts"; +export type { + SetReferrerOptions, + SetReferrerSuccessResponse, + SetReferrerParameters, +} from "../api/exchange/_methods/setReferrer.ts"; diff --git a/packages/hyperliquid/src/actions/spotDeploy.ts b/packages/hyperliquid/src/actions/spotDeploy.ts new file mode 100644 index 00000000..3d19ea0f --- /dev/null +++ b/packages/hyperliquid/src/actions/spotDeploy.ts @@ -0,0 +1,7 @@ +/** Canonical spotDeploy action builder. @module */ +export { buildSpotDeploy } from "../api/exchange/_methods/spotDeploy.ts"; +export type { + SpotDeployOptions, + SpotDeploySuccessResponse, + SpotDeployParameters, +} from "../api/exchange/_methods/spotDeploy.ts"; diff --git a/packages/hyperliquid/src/actions/spotSend.ts b/packages/hyperliquid/src/actions/spotSend.ts new file mode 100644 index 00000000..a837e8f1 --- /dev/null +++ b/packages/hyperliquid/src/actions/spotSend.ts @@ -0,0 +1,7 @@ +/** Canonical spotSend action builder. @module */ +export { buildSpotSend } from "../api/exchange/_methods/spotSend.ts"; +export type { + SpotSendOptions, + SpotSendSuccessResponse, + SpotSendParameters, +} from "../api/exchange/_methods/spotSend.ts"; diff --git a/packages/hyperliquid/src/actions/spotUser.ts b/packages/hyperliquid/src/actions/spotUser.ts new file mode 100644 index 00000000..c8fe4c82 --- /dev/null +++ b/packages/hyperliquid/src/actions/spotUser.ts @@ -0,0 +1,7 @@ +/** Canonical spotUser action builder. @module */ +export { buildSpotUser } from "../api/exchange/_methods/spotUser.ts"; +export type { + SpotUserOptions, + SpotUserSuccessResponse, + SpotUserParameters, +} from "../api/exchange/_methods/spotUser.ts"; diff --git a/packages/hyperliquid/src/actions/stakingDeposit.ts b/packages/hyperliquid/src/actions/stakingDeposit.ts new file mode 100644 index 00000000..8b7d0b63 --- /dev/null +++ b/packages/hyperliquid/src/actions/stakingDeposit.ts @@ -0,0 +1,3 @@ +/** Canonical stakingDeposit action builder alias. @module */ +export { buildStakingDeposit } from "../api/exchange/_methods/stakingDeposit.ts"; +export type * from "../api/exchange/_methods/stakingDeposit.ts"; diff --git a/packages/hyperliquid/src/actions/stakingLinkDisableTradingUser.ts b/packages/hyperliquid/src/actions/stakingLinkDisableTradingUser.ts new file mode 100644 index 00000000..4a07a3b7 --- /dev/null +++ b/packages/hyperliquid/src/actions/stakingLinkDisableTradingUser.ts @@ -0,0 +1,7 @@ +/** Canonical stakingLinkDisableTradingUser action builder. @module */ +export { buildStakingLinkDisableTradingUser } from "../api/exchange/_methods/stakingLinkDisableTradingUser.ts"; +export type { + StakingLinkDisableTradingUserOptions, + StakingLinkDisableTradingUserSuccessResponse, + StakingLinkDisableTradingUserParameters, +} from "../api/exchange/_methods/stakingLinkDisableTradingUser.ts"; diff --git a/packages/hyperliquid/src/actions/stakingWithdraw.ts b/packages/hyperliquid/src/actions/stakingWithdraw.ts new file mode 100644 index 00000000..10840bea --- /dev/null +++ b/packages/hyperliquid/src/actions/stakingWithdraw.ts @@ -0,0 +1,3 @@ +/** Canonical stakingWithdraw action builder alias. @module */ +export { buildStakingWithdraw } from "../api/exchange/_methods/stakingWithdraw.ts"; +export type * from "../api/exchange/_methods/stakingWithdraw.ts"; diff --git a/packages/hyperliquid/src/actions/subAccountModify.ts b/packages/hyperliquid/src/actions/subAccountModify.ts new file mode 100644 index 00000000..6c67e335 --- /dev/null +++ b/packages/hyperliquid/src/actions/subAccountModify.ts @@ -0,0 +1,7 @@ +/** Canonical subAccountModify action builder. @module */ +export { buildSubAccountModify } from "../api/exchange/_methods/subAccountModify.ts"; +export type { + SubAccountModifyOptions, + SubAccountModifySuccessResponse, + SubAccountModifyParameters, +} from "../api/exchange/_methods/subAccountModify.ts"; diff --git a/packages/hyperliquid/src/actions/subAccountSpotTransfer.ts b/packages/hyperliquid/src/actions/subAccountSpotTransfer.ts new file mode 100644 index 00000000..40ead029 --- /dev/null +++ b/packages/hyperliquid/src/actions/subAccountSpotTransfer.ts @@ -0,0 +1,7 @@ +/** Canonical subAccountSpotTransfer action builder. @module */ +export { buildSubAccountSpotTransfer } from "../api/exchange/_methods/subAccountSpotTransfer.ts"; +export type { + SubAccountSpotTransferOptions, + SubAccountSpotTransferSuccessResponse, + SubAccountSpotTransferParameters, +} from "../api/exchange/_methods/subAccountSpotTransfer.ts"; diff --git a/packages/hyperliquid/src/actions/subAccountTransfer.ts b/packages/hyperliquid/src/actions/subAccountTransfer.ts new file mode 100644 index 00000000..a7729fa5 --- /dev/null +++ b/packages/hyperliquid/src/actions/subAccountTransfer.ts @@ -0,0 +1,7 @@ +/** Canonical subAccountTransfer action builder. @module */ +export { buildSubAccountTransfer } from "../api/exchange/_methods/subAccountTransfer.ts"; +export type { + SubAccountTransferOptions, + SubAccountTransferSuccessResponse, + SubAccountTransferParameters, +} from "../api/exchange/_methods/subAccountTransfer.ts"; diff --git a/packages/hyperliquid/src/actions/tokenDelegate.ts b/packages/hyperliquid/src/actions/tokenDelegate.ts new file mode 100644 index 00000000..dc089689 --- /dev/null +++ b/packages/hyperliquid/src/actions/tokenDelegate.ts @@ -0,0 +1,7 @@ +/** Canonical tokenDelegate action builder. @module */ +export { buildTokenDelegate } from "../api/exchange/_methods/tokenDelegate.ts"; +export type { + TokenDelegateOptions, + TokenDelegateSuccessResponse, + TokenDelegateParameters, +} from "../api/exchange/_methods/tokenDelegate.ts"; diff --git a/packages/hyperliquid/src/actions/topUpIsolatedOnlyMargin.ts b/packages/hyperliquid/src/actions/topUpIsolatedOnlyMargin.ts new file mode 100644 index 00000000..5eae416a --- /dev/null +++ b/packages/hyperliquid/src/actions/topUpIsolatedOnlyMargin.ts @@ -0,0 +1,7 @@ +/** Canonical topUpIsolatedOnlyMargin action builder. @module */ +export { buildTopUpIsolatedOnlyMargin } from "../api/exchange/_methods/topUpIsolatedOnlyMargin.ts"; +export type { + TopUpIsolatedOnlyMarginOptions, + TopUpIsolatedOnlyMarginSuccessResponse, + TopUpIsolatedOnlyMarginParameters, +} from "../api/exchange/_methods/topUpIsolatedOnlyMargin.ts"; diff --git a/packages/hyperliquid/src/actions/twapCancel.ts b/packages/hyperliquid/src/actions/twapCancel.ts new file mode 100644 index 00000000..99df92be --- /dev/null +++ b/packages/hyperliquid/src/actions/twapCancel.ts @@ -0,0 +1,7 @@ +/** Canonical twapCancel action builder. @module */ +export { buildTwapCancel } from "../api/exchange/_methods/twapCancel.ts"; +export type { + TwapCancelOptions, + TwapCancelSuccessResponse, + TwapCancelParameters, +} from "../api/exchange/_methods/twapCancel.ts"; diff --git a/packages/hyperliquid/src/actions/twapOrder.ts b/packages/hyperliquid/src/actions/twapOrder.ts new file mode 100644 index 00000000..466e3100 --- /dev/null +++ b/packages/hyperliquid/src/actions/twapOrder.ts @@ -0,0 +1,7 @@ +/** Canonical twapOrder action builder. @module */ +export { buildTwapOrder } from "../api/exchange/_methods/twapOrder.ts"; +export type { + TwapOrderOptions, + TwapOrderSuccessResponse, + TwapOrderParameters, +} from "../api/exchange/_methods/twapOrder.ts"; diff --git a/packages/hyperliquid/src/actions/updateIsolatedMargin.ts b/packages/hyperliquid/src/actions/updateIsolatedMargin.ts new file mode 100644 index 00000000..8895b8d3 --- /dev/null +++ b/packages/hyperliquid/src/actions/updateIsolatedMargin.ts @@ -0,0 +1,7 @@ +/** Canonical updateIsolatedMargin action builder. @module */ +export { buildUpdateIsolatedMargin } from "../api/exchange/_methods/updateIsolatedMargin.ts"; +export type { + UpdateIsolatedMarginOptions, + UpdateIsolatedMarginSuccessResponse, + UpdateIsolatedMarginParameters, +} from "../api/exchange/_methods/updateIsolatedMargin.ts"; diff --git a/packages/hyperliquid/src/actions/updateLeverage.ts b/packages/hyperliquid/src/actions/updateLeverage.ts new file mode 100644 index 00000000..03643be7 --- /dev/null +++ b/packages/hyperliquid/src/actions/updateLeverage.ts @@ -0,0 +1,7 @@ +/** Canonical updateLeverage action builder. @module */ +export { buildUpdateLeverage } from "../api/exchange/_methods/updateLeverage.ts"; +export type { + UpdateLeverageOptions, + UpdateLeverageSuccessResponse, + UpdateLeverageParameters, +} from "../api/exchange/_methods/updateLeverage.ts"; diff --git a/packages/hyperliquid/src/actions/usdClassTransfer.ts b/packages/hyperliquid/src/actions/usdClassTransfer.ts new file mode 100644 index 00000000..f7f03a9e --- /dev/null +++ b/packages/hyperliquid/src/actions/usdClassTransfer.ts @@ -0,0 +1,7 @@ +/** Canonical usdClassTransfer action builder. @module */ +export { buildUsdClassTransfer } from "../api/exchange/_methods/usdClassTransfer.ts"; +export type { + UsdClassTransferOptions, + UsdClassTransferSuccessResponse, + UsdClassTransferParameters, +} from "../api/exchange/_methods/usdClassTransfer.ts"; diff --git a/packages/hyperliquid/src/actions/usdSend.ts b/packages/hyperliquid/src/actions/usdSend.ts new file mode 100644 index 00000000..8ca95245 --- /dev/null +++ b/packages/hyperliquid/src/actions/usdSend.ts @@ -0,0 +1,3 @@ +/** Canonical usdSend action builder. @module */ +export { buildUsdSend } from "../api/exchange/_methods/usdSend.ts"; +export type { UsdSendOptions, UsdSendSuccessResponse, UsdSendParameters } from "../api/exchange/_methods/usdSend.ts"; diff --git a/packages/hyperliquid/src/actions/userDexAbstraction.ts b/packages/hyperliquid/src/actions/userDexAbstraction.ts new file mode 100644 index 00000000..c5157f3a --- /dev/null +++ b/packages/hyperliquid/src/actions/userDexAbstraction.ts @@ -0,0 +1,7 @@ +/** Canonical userDexAbstraction action builder. @module */ +export { buildUserDexAbstraction } from "../api/exchange/_methods/userDexAbstraction.ts"; +export type { + UserDexAbstractionOptions, + UserDexAbstractionSuccessResponse, + UserDexAbstractionParameters, +} from "../api/exchange/_methods/userDexAbstraction.ts"; diff --git a/packages/hyperliquid/src/actions/userOutcome.ts b/packages/hyperliquid/src/actions/userOutcome.ts new file mode 100644 index 00000000..ccf78b6c --- /dev/null +++ b/packages/hyperliquid/src/actions/userOutcome.ts @@ -0,0 +1,7 @@ +/** Canonical userOutcome action builder. @module */ +export { buildUserOutcome } from "../api/exchange/_methods/userOutcome.ts"; +export type { + UserOutcomeOptions, + UserOutcomeSuccessResponse, + UserOutcomeParameters, +} from "../api/exchange/_methods/userOutcome.ts"; diff --git a/packages/hyperliquid/src/actions/userPortfolioMargin.ts b/packages/hyperliquid/src/actions/userPortfolioMargin.ts new file mode 100644 index 00000000..568e0eff --- /dev/null +++ b/packages/hyperliquid/src/actions/userPortfolioMargin.ts @@ -0,0 +1,7 @@ +/** Canonical userPortfolioMargin action builder. @module */ +export { buildUserPortfolioMargin } from "../api/exchange/_methods/userPortfolioMargin.ts"; +export type { + UserPortfolioMarginOptions, + UserPortfolioMarginSuccessResponse, + UserPortfolioMarginParameters, +} from "../api/exchange/_methods/userPortfolioMargin.ts"; diff --git a/packages/hyperliquid/src/actions/userSetAbstraction.ts b/packages/hyperliquid/src/actions/userSetAbstraction.ts new file mode 100644 index 00000000..512900b1 --- /dev/null +++ b/packages/hyperliquid/src/actions/userSetAbstraction.ts @@ -0,0 +1,7 @@ +/** Canonical userSetAbstraction action builder. @module */ +export { buildUserSetAbstraction } from "../api/exchange/_methods/userSetAbstraction.ts"; +export type { + UserSetAbstractionOptions, + UserSetAbstractionSuccessResponse, + UserSetAbstractionParameters, +} from "../api/exchange/_methods/userSetAbstraction.ts"; diff --git a/packages/hyperliquid/src/actions/validatorAction.ts b/packages/hyperliquid/src/actions/validatorAction.ts new file mode 100644 index 00000000..443d3c29 --- /dev/null +++ b/packages/hyperliquid/src/actions/validatorAction.ts @@ -0,0 +1,3 @@ +/** Canonical validatorAction action builder alias. @module */ +export { buildValidatorAction } from "../api/exchange/_methods/validatorAction.ts"; +export type * from "../api/exchange/_methods/validatorAction.ts"; diff --git a/packages/hyperliquid/src/actions/validatorL1Stream.ts b/packages/hyperliquid/src/actions/validatorL1Stream.ts new file mode 100644 index 00000000..a9f167b7 --- /dev/null +++ b/packages/hyperliquid/src/actions/validatorL1Stream.ts @@ -0,0 +1,7 @@ +/** Canonical validatorL1Stream action builder. @module */ +export { buildValidatorL1Stream } from "../api/exchange/_methods/validatorL1Stream.ts"; +export type { + ValidatorL1StreamOptions, + ValidatorL1StreamSuccessResponse, + ValidatorL1StreamParameters, +} from "../api/exchange/_methods/validatorL1Stream.ts"; diff --git a/packages/hyperliquid/src/actions/validatorSignerAction.ts b/packages/hyperliquid/src/actions/validatorSignerAction.ts new file mode 100644 index 00000000..03bc27b6 --- /dev/null +++ b/packages/hyperliquid/src/actions/validatorSignerAction.ts @@ -0,0 +1,3 @@ +/** Canonical validatorSignerAction action builder alias. @module */ +export { buildValidatorSignerAction } from "../api/exchange/_methods/validatorSignerAction.ts"; +export type * from "../api/exchange/_methods/validatorSignerAction.ts"; diff --git a/packages/hyperliquid/src/actions/vaultDistribute.ts b/packages/hyperliquid/src/actions/vaultDistribute.ts new file mode 100644 index 00000000..d0c664b9 --- /dev/null +++ b/packages/hyperliquid/src/actions/vaultDistribute.ts @@ -0,0 +1,7 @@ +/** Canonical vaultDistribute action builder. @module */ +export { buildVaultDistribute } from "../api/exchange/_methods/vaultDistribute.ts"; +export type { + VaultDistributeOptions, + VaultDistributeSuccessResponse, + VaultDistributeParameters, +} from "../api/exchange/_methods/vaultDistribute.ts"; diff --git a/packages/hyperliquid/src/actions/vaultModify.ts b/packages/hyperliquid/src/actions/vaultModify.ts new file mode 100644 index 00000000..09e1e890 --- /dev/null +++ b/packages/hyperliquid/src/actions/vaultModify.ts @@ -0,0 +1,7 @@ +/** Canonical vaultModify action builder. @module */ +export { buildVaultModify } from "../api/exchange/_methods/vaultModify.ts"; +export type { + VaultModifyOptions, + VaultModifySuccessResponse, + VaultModifyParameters, +} from "../api/exchange/_methods/vaultModify.ts"; diff --git a/packages/hyperliquid/src/actions/vaultTransfer.ts b/packages/hyperliquid/src/actions/vaultTransfer.ts new file mode 100644 index 00000000..4e3e4241 --- /dev/null +++ b/packages/hyperliquid/src/actions/vaultTransfer.ts @@ -0,0 +1,7 @@ +/** Canonical vaultTransfer action builder. @module */ +export { buildVaultTransfer } from "../api/exchange/_methods/vaultTransfer.ts"; +export type { + VaultTransferOptions, + VaultTransferSuccessResponse, + VaultTransferParameters, +} from "../api/exchange/_methods/vaultTransfer.ts"; diff --git a/packages/hyperliquid/src/actions/withdraw.ts b/packages/hyperliquid/src/actions/withdraw.ts new file mode 100644 index 00000000..7f45da75 --- /dev/null +++ b/packages/hyperliquid/src/actions/withdraw.ts @@ -0,0 +1,3 @@ +/** Canonical withdraw action builder alias. @module */ +export { buildWithdraw } from "../api/exchange/_methods/withdraw.ts"; +export type * from "../api/exchange/_methods/withdraw.ts"; diff --git a/packages/hyperliquid/src/actions/withdraw3.ts b/packages/hyperliquid/src/actions/withdraw3.ts new file mode 100644 index 00000000..6a2fdc30 --- /dev/null +++ b/packages/hyperliquid/src/actions/withdraw3.ts @@ -0,0 +1,7 @@ +/** Canonical withdraw3 action builder. @module */ +export { buildWithdraw3 } from "../api/exchange/_methods/withdraw3.ts"; +export type { + Withdraw3Options, + Withdraw3SuccessResponse, + Withdraw3Parameters, +} from "../api/exchange/_methods/withdraw3.ts"; diff --git a/packages/hyperliquid/src/api/exchange/_methods/_base/_action.ts b/packages/hyperliquid/src/api/exchange/_methods/_base/_action.ts index abe90483..35e2031e 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/_base/_action.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/_base/_action.ts @@ -5,7 +5,7 @@ import type * as v from "valibot"; import { parse } from "../../../../_base.ts"; -import { canonicalize } from "../../../../signing/mod.ts"; +import { canonicalize } from "../../../../signing/_canonicalize.ts"; /** * Builds the canonical wire action for an Exchange API method. diff --git a/packages/hyperliquid/src/api/exchange/_methods/_base/_shell.ts b/packages/hyperliquid/src/api/exchange/_methods/_base/_shell.ts index 0a665a08..b44c356f 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/_base/_shell.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/_base/_shell.ts @@ -1,3 +1,5 @@ +import { race } from "../../../../transport/_abort.ts"; +import { registerExchangeWireRequest } from "../../../../transport/_wire.ts"; /** * Common execution shell shared by L1 and user-signed Exchange API actions. * @module @@ -48,6 +50,18 @@ export interface PreparedExchangeRequest { readonly __responseType?: T; } +const signingContexts = new WeakMap(); + +/** Internal ownership context of a directly prepared request, outside the wire body. */ +export function getSigningContext(request: object): { key: string; isTestnet: boolean } | undefined { + return signingContexts.get(request); +} + +/** Preserve known signer ownership when a directly prepared request is copied and frozen. */ +export function linkSigningContext(request: object, context: { key: string; isTestnet: boolean }): void { + signingContexts.set(request, context); +} + // ============================================================ // Prepared-payload poison state (internal) // ============================================================ @@ -102,7 +116,7 @@ const nonceKeyCache = new WeakMap< * nonce has been handed to the transport. * * The nonce lock guarantees the order nonces are ISSUED in; this guarantees the order they reach - * the WIRE in, which is what the server actually requires. Keeping the two separate is what lets + * the WIRE in, the SDK's compatibility policy, rather than a protocol requirement. Keeping the two separate is what lets * signing — a network round trip for any remote wallet — run outside the lock and overlap across * callers, while a later nonce still cannot overtake an earlier one. * @@ -122,8 +136,8 @@ const dispatchChains = new Map>(); * difference between one order in flight per wallet and all of them. * * Wire order is preserved by {@linkcode dispatchChains} rather than by the lock: a request waits - * for its predecessor to reach `transport.request` before making its own call, so the server still - * sees strictly increasing nonces per wallet. Network responses resolve concurrently. + * for its predecessor to reach `transport.request` before making its own call, so the default policy + * preserves nonce issuance order. Network responses resolve concurrently. * * @param config Exchange API configuration. * @param build Callback that, given the nonce, returns the action, signature, and any extras. @@ -136,11 +150,12 @@ export async function executeWithShell( config: ExchangeConfig, build: (nonce: number) => Promise, signal?: AbortSignal, + prepareOnly = false, ): Promise { const leader = "wallet" in config ? config.wallet : config.signers[0]; const walletAddress = await getWalletAddress(leader); - // Lock per (wallet × testnet) ensures requests are dispatched to the server in nonce order. + // Serialize nonce allocation per (wallet × testnet); managed execution preserves delivery order. // The key string is cached per (wallet × transport); it is rebuilt only when the wallet's // address or the transport's testnet flag no longer matches the cached entry. const isTestnet = config.transport.isTestnet; @@ -157,6 +172,7 @@ export async function executeWithShell( } perTransport.set(config.transport, { walletAddress, isTestnet, key }); } + if (signal?.aborted) throw signal.reason; const box = await withLock(key, async () => { // --- Generate nonce -------------------------------------- // `globalNonceManager.getNonce` returns a plain number: skip the await (and its async hop) @@ -166,12 +182,12 @@ export async function executeWithShell( // --- Claim this nonce's slot in the dispatch order -------- // Taken under the lock, so slots are claimed in the same order nonces are issued. - const predecessor = dispatchChains.get(key); + const predecessor = prepareOnly ? undefined : dispatchChains.get(key); let openGate!: () => void; const dispatched = new Promise((resolve) => { openGate = resolve; }); - dispatchChains.set(key, dispatched); + if (!prepareOnly) dispatchChains.set(key, dispatched); // --- Sign and dispatch, outside the lock ------------------ // Signing is a network round trip for a remote wallet; running it here rather than inside the @@ -180,12 +196,21 @@ export async function executeWithShell( const pending = (async (): Promise => { let response: Promise | undefined; try { - const { action, signature, extras } = await build(nonce); - if (predecessor !== undefined) await predecessor; + const { action, signature, extras } = await race(build(nonce), signal); + if (signal?.aborted) throw signal.reason; + if (prepareOnly) { + const request = { action, signature, nonce, ...extras }; + registerExchangeWireRequest(request, action); + signingContexts.set(request, { key, isTestnet }); + return request as T; + } + if (predecessor !== undefined) await race(predecessor, signal); // `transport.request` runs synchronously up to its first await, so wire order is fixed // here. It is assigned rather than awaited so the gate below opens on dispatch, not on // the response. - response = config.transport.request("exchange", { action, signature, nonce, ...extras }, signal); + const request = { action, signature, nonce, ...extras }; + registerExchangeWireRequest(request, action); + response = config.transport.request("exchange", request, signal); } finally { // Wait for our turn even when this request never reached the wire. A rejected signature // or an abort burns its nonce, which the server tolerates as a gap — but opening the gate @@ -206,6 +231,6 @@ export async function executeWithShell( // --- Await response (concurrently across calls) and validate const response = await box.pending; - assertSuccessResponse(response); + if (!prepareOnly) assertSuccessResponse(response); return response; } diff --git a/packages/hyperliquid/src/api/exchange/_methods/_base/execute.ts b/packages/hyperliquid/src/api/exchange/_methods/_base/execute.ts index 6b0d3880..50131f73 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/_base/execute.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/_base/execute.ts @@ -57,6 +57,7 @@ export function executeL1Action( /** Skip validation of `vaultAddress`/`expiresAfter`. See the `skipValidation` option in `_config.ts`. */ skipValidation?: boolean; }, + prepareOnly = false, ): Promise { // Resolve options synchronously on the common path (no function-valued `defaultExpiresAfter`), // returning the inner promise directly instead of paying the extra async hops of an `async` @@ -70,10 +71,10 @@ export function executeL1Action( // Rare path: a function-valued default may resolve asynchronously. A sync throw from the // function itself lands in the catch below and becomes a rejection, as before. return Promise.resolve(expiresAfterInput()).then((resolved) => - executeL1ActionResolved(config, action, vaultAddressInput, resolved, options), + executeL1ActionResolved(config, action, vaultAddressInput, resolved, options, prepareOnly), ); } - return executeL1ActionResolved(config, action, vaultAddressInput, expiresAfterInput, options); + return executeL1ActionResolved(config, action, vaultAddressInput, expiresAfterInput, options, prepareOnly); } catch (error) { return Promise.reject(error); } @@ -89,6 +90,7 @@ function executeL1ActionResolved( signal?: AbortSignal; skipValidation?: boolean; }, + prepareOnly = false, ): Promise { const vaultAddress = options?.skipValidation ? (vaultAddressInput as `0x${string}` | undefined) @@ -104,22 +106,24 @@ function executeL1ActionResolved( return executeWithShell( config, async (nonce) => { + const wireAction = + action.type === "createVault" || action.type === "agentSendAsset" ? { ...action, nonce } : action; if ("wallet" in config) { const signature = await signL1Action({ wallet: config.wallet, - action, + action: wireAction, nonce, isTestnet: config.transport.isTestnet, vaultAddress, expiresAfter, }); - return { action, signature, extras: { vaultAddress, expiresAfter } }; + return { action: wireAction, signature, extras: { vaultAddress, expiresAfter } }; } else { const { action: wrapper, signature } = await signMultiSigL1({ signers: config.signers, multiSigUser: config.multiSigUser, signatureChainId: await resolveSignatureChainId(config), - action, + action: wireAction, nonce, isTestnet: config.transport.isTestnet, vaultAddress, @@ -129,6 +133,7 @@ function executeL1ActionResolved( } }, options?.signal, + prepareOnly, ); } @@ -157,8 +162,11 @@ export function executeUserSignedAction( options?: { /** Transforms the completed action into its outer multi-sig payload representation. */ toMultiSigPayloadAction?: (action: Readonly>) => Record; + /** Transform the single-wallet action after signing, for protocol-specific wire fields. */ + toSinglePayloadAction?: (action: Readonly>) => Record; signal?: AbortSignal; }, + prepareOnly = false, ): Promise { return executeWithShell( config, @@ -183,7 +191,7 @@ export function executeUserSignedAction( action: fullAction, types, }); - return { action: fullAction, signature }; + return { action: options?.toSinglePayloadAction?.(fullAction) ?? fullAction, signature }; } else { const payloadAction = options?.toMultiSigPayloadAction?.(fullAction) ?? fullAction; const { action: wrapper, signature } = await signMultiSigUserSigned({ @@ -197,6 +205,7 @@ export function executeUserSignedAction( } }, options?.signal, + prepareOnly, ); } diff --git a/packages/hyperliquid/src/api/exchange/_methods/activateOutcomeDeployer.ts b/packages/hyperliquid/src/api/exchange/_methods/activateOutcomeDeployer.ts index 5398a16e..ccda2ac0 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/activateOutcomeDeployer.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/activateOutcomeDeployer.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -141,3 +142,11 @@ export function activateOutcomeDeployer( const action = buildAction(ActivateOutcomeDeployerActionSchema, { type: "activateOutcomeDeployer", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical activateOutcomeDeployer action without allocating a nonce or calling a wallet/transport. */ +export function buildActivateOutcomeDeployer( + params: ActivateOutcomeDeployerParameters, +): CanonicalAction { + const action = buildAction(ActivateOutcomeDeployerActionSchema, { type: "activateOutcomeDeployer", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/agentEnableDexAbstraction.ts b/packages/hyperliquid/src/api/exchange/_methods/agentEnableDexAbstraction.ts index e283fe03..45fc6ab9 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/agentEnableDexAbstraction.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/agentEnableDexAbstraction.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -116,3 +117,9 @@ export function agentEnableDexAbstraction( const action = buildAction(AgentEnableDexAbstractionActionSchema, { type: "agentEnableDexAbstraction" }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical agentEnableDexAbstraction action without allocating a nonce or calling a wallet/transport. */ +export function buildAgentEnableDexAbstraction(): CanonicalAction { + const action = buildAction(AgentEnableDexAbstractionActionSchema, { type: "agentEnableDexAbstraction" }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/agentSendAsset.ts b/packages/hyperliquid/src/api/exchange/_methods/agentSendAsset.ts index de8945d0..193eecd2 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/agentSendAsset.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/agentSendAsset.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -153,3 +154,9 @@ export function agentSendAsset( opts, ); } + +/** Validate and own a canonical agentSendAsset action without allocating a nonce or calling a wallet/transport. */ +export function buildAgentSendAsset(params: AgentSendAssetParameters): CanonicalAction { + const action = buildAction(AgentSendAssetActionSchema, { type: "agentSendAsset", ...params, nonce: 0 }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/agentSetAbstraction.ts b/packages/hyperliquid/src/api/exchange/_methods/agentSetAbstraction.ts index bdd8af31..211c9d97 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/agentSetAbstraction.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/agentSetAbstraction.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -131,3 +132,11 @@ export function agentSetAbstraction( const action = buildAction(AgentSetAbstractionActionSchema, { type: "agentSetAbstraction", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical agentSetAbstraction action without allocating a nonce or calling a wallet/transport. */ +export function buildAgentSetAbstraction( + params: AgentSetAbstractionParameters, +): CanonicalAction { + const action = buildAction(AgentSetAbstractionActionSchema, { type: "agentSetAbstraction", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/approveAgent.ts b/packages/hyperliquid/src/api/exchange/_methods/approveAgent.ts index c14c0ce5..8748035d 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/approveAgent.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/approveAgent.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -244,3 +245,19 @@ function omitUnnamedAgentName(transport: IRequestTransport): IRequestTransport { function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } + +/** Validate and own a canonical approveAgent action without allocating a nonce or calling a wallet/transport. */ +export function buildApproveAgent(params: ApproveAgentParameters): CanonicalAction { + // Docs: an agent's `valid_until` expiration can be at most 180 days in the future. Guarded only + // when a timestamp is present — a name without `valid_until` (or an unnamed agent) carries no + // expiration. Cheap deterministic guard; runs even when `skipValidation` is set. + const validUntil = typeof params.agentName === "string" ? VALID_UNTIL_PATTERN.exec(params.agentName)?.[1] : undefined; + if (validUntil !== undefined) parse(AgentExpirationSchema, validUntil); + const action = buildAction(ApproveAgentActionSchema, { type: "approveAgent", ...params }); + return canonicalAction(action, { + kind: "user", + types: ApproveAgentTypes, + toMultiSigPayloadAction: action.agentName === "" ? omitAgentName : undefined, + toSinglePayloadAction: action.agentName === "" ? omitAgentName : undefined, + }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/approveBuilderFee.ts b/packages/hyperliquid/src/api/exchange/_methods/approveBuilderFee.ts index 02b235b3..0789c4e2 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/approveBuilderFee.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/approveBuilderFee.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -142,3 +143,11 @@ export function approveBuilderFee( const action = buildAction(ApproveBuilderFeeActionSchema, { type: "approveBuilderFee", ...params }, opts); return executeUserSignedAction(config, action, ApproveBuilderFeeTypes, opts); } + +/** Validate and own a canonical approveBuilderFee action without allocating a nonce or calling a wallet/transport. */ +export function buildApproveBuilderFee( + params: ApproveBuilderFeeParameters, +): CanonicalAction { + const action = buildAction(ApproveBuilderFeeActionSchema, { type: "approveBuilderFee", ...params }); + return canonicalAction(action, { kind: "user", types: ApproveBuilderFeeTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/authorizeAqav2Role.ts b/packages/hyperliquid/src/api/exchange/_methods/authorizeAqav2Role.ts index 81f4d529..e19e5001 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/authorizeAqav2Role.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/authorizeAqav2Role.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -124,3 +125,11 @@ export function authorizeAqav2Role( const action = buildAction(AuthorizeAqav2RoleActionSchema, { type: "authorizeAqav2Role", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical authorizeAqav2Role action without allocating a nonce or calling a wallet/transport. */ +export function buildAuthorizeAqav2Role( + params: AuthorizeAqav2RoleParameters, +): CanonicalAction { + const action = buildAction(AuthorizeAqav2RoleActionSchema, { type: "authorizeAqav2Role", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/batchModify.ts b/packages/hyperliquid/src/api/exchange/_methods/batchModify.ts index 4130ec5e..db61c45a 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/batchModify.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/batchModify.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -181,3 +182,9 @@ export function batchModify( const action = buildAction(BatchModifyActionSchema, { type: "batchModify", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical batchModify action without allocating a nonce or calling a wallet/transport. */ +export function buildBatchModify(params: BatchModifyParameters): CanonicalAction { + const action = buildAction(BatchModifyActionSchema, { type: "batchModify", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/borrowLend.ts b/packages/hyperliquid/src/api/exchange/_methods/borrowLend.ts index cdd01318..f9ffb778 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/borrowLend.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/borrowLend.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -125,3 +126,9 @@ export function borrowLend( const action = buildAction(BorrowLendActionSchema, { type: "borrowLend", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical borrowLend action without allocating a nonce or calling a wallet/transport. */ +export function buildBorrowLend(params: BorrowLendParameters): CanonicalAction { + const action = buildAction(BorrowLendActionSchema, { type: "borrowLend", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/cDeposit.ts b/packages/hyperliquid/src/api/exchange/_methods/cDeposit.ts index 2ca16792..f714ae98 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/cDeposit.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/cDeposit.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -136,3 +137,9 @@ export function cDeposit( const action = buildAction(CDepositActionSchema, { type: "cDeposit", ...params }, opts); return executeUserSignedAction(config, action, CDepositTypes, opts); } + +/** Validate and own a canonical cDeposit action without allocating a nonce or calling a wallet/transport. */ +export function buildCDeposit(params: CDepositParameters): CanonicalAction { + const action = buildAction(CDepositActionSchema, { type: "cDeposit", ...params }); + return canonicalAction(action, { kind: "user", types: CDepositTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/cSignerAction.ts b/packages/hyperliquid/src/api/exchange/_methods/cSignerAction.ts index 9f09d85f..55bd014c 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/cSignerAction.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/cSignerAction.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -146,3 +147,9 @@ export function cSignerAction( const action = buildAction(CSignerActionActionSchema, { type: "CSignerAction", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical cSignerAction action without allocating a nonce or calling a wallet/transport. */ +export function buildCSignerAction(params: CSignerActionParameters): CanonicalAction { + const action = buildAction(CSignerActionActionSchema, { type: "CSignerAction", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/cValidatorAction.ts b/packages/hyperliquid/src/api/exchange/_methods/cValidatorAction.ts index f488796a..c7917c89 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/cValidatorAction.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/cValidatorAction.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -189,3 +190,11 @@ export function cValidatorAction( const action = buildAction(CValidatorActionActionSchema, { type: "CValidatorAction", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical cValidatorAction action without allocating a nonce or calling a wallet/transport. */ +export function buildCValidatorAction( + params: CValidatorActionParameters, +): CanonicalAction { + const action = buildAction(CValidatorActionActionSchema, { type: "CValidatorAction", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/cWithdraw.ts b/packages/hyperliquid/src/api/exchange/_methods/cWithdraw.ts index 3c9d9f3a..db659d18 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/cWithdraw.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/cWithdraw.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -136,3 +137,9 @@ export function cWithdraw( const action = buildAction(CWithdrawActionSchema, { type: "cWithdraw", ...params }, opts); return executeUserSignedAction(config, action, CWithdrawTypes, opts); } + +/** Validate and own a canonical cWithdraw action without allocating a nonce or calling a wallet/transport. */ +export function buildCWithdraw(params: CWithdrawParameters): CanonicalAction { + const action = buildAction(CWithdrawActionSchema, { type: "cWithdraw", ...params }); + return canonicalAction(action, { kind: "user", types: CWithdrawTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/cancel.ts b/packages/hyperliquid/src/api/exchange/_methods/cancel.ts index 3e67d4e2..e75088ba 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/cancel.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/cancel.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -143,3 +144,9 @@ export function cancel( const action = buildAction(CancelActionSchema, { type: "cancel", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical cancel action without allocating a nonce or calling a wallet/transport. */ +export function buildCancel(params: CancelParameters): CanonicalAction { + const action = buildAction(CancelActionSchema, { type: "cancel", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/cancelByCloid.ts b/packages/hyperliquid/src/api/exchange/_methods/cancelByCloid.ts index 31f9607c..249e0e82 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/cancelByCloid.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/cancelByCloid.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -120,3 +121,9 @@ export function cancelByCloid( const action = buildAction(CancelByCloidActionSchema, { type: "cancelByCloid", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical cancelByCloid action without allocating a nonce or calling a wallet/transport. */ +export function buildCancelByCloid(params: CancelByCloidParameters): CanonicalAction { + const action = buildAction(CancelByCloidActionSchema, { type: "cancelByCloid", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/claimRewards.ts b/packages/hyperliquid/src/api/exchange/_methods/claimRewards.ts index 95b49c2e..0c226845 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/claimRewards.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/claimRewards.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -109,3 +110,9 @@ export function claimRewards(config: ExchangeConfig, opts?: ClaimRewardsOptions) const action = buildAction(ClaimRewardsActionSchema, { type: "claimRewards" }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical claimRewards action without allocating a nonce or calling a wallet/transport. */ +export function buildClaimRewards(): CanonicalAction { + const action = buildAction(ClaimRewardsActionSchema, { type: "claimRewards" }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/convertToMultiSigUser.ts b/packages/hyperliquid/src/api/exchange/_methods/convertToMultiSigUser.ts index 6c88bc4c..295dd3d5 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/convertToMultiSigUser.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/convertToMultiSigUser.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -182,3 +183,11 @@ export function convertToMultiSigUser( const action = buildAction(ConvertToMultiSigUserActionSchema, { type: "convertToMultiSigUser", ...params }, opts); return executeUserSignedAction(config, action, ConvertToMultiSigUserTypes, opts); } + +/** Validate and own a canonical convertToMultiSigUser action without allocating a nonce or calling a wallet/transport. */ +export function buildConvertToMultiSigUser( + params: ConvertToMultiSigUserParameters, +): CanonicalAction { + const action = buildAction(ConvertToMultiSigUserActionSchema, { type: "convertToMultiSigUser", ...params }); + return canonicalAction(action, { kind: "user", types: ConvertToMultiSigUserTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/createSubAccount.ts b/packages/hyperliquid/src/api/exchange/_methods/createSubAccount.ts index f5737183..7fca216e 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/createSubAccount.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/createSubAccount.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -122,3 +123,11 @@ export function createSubAccount( const action = buildAction(CreateSubAccountActionSchema, { type: "createSubAccount", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical createSubAccount action without allocating a nonce or calling a wallet/transport. */ +export function buildCreateSubAccount( + params: CreateSubAccountParameters, +): CanonicalAction { + const action = buildAction(CreateSubAccountActionSchema, { type: "createSubAccount", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/createVault.ts b/packages/hyperliquid/src/api/exchange/_methods/createVault.ts index 546f250c..e02ecf78 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/createVault.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/createVault.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -141,3 +142,9 @@ export function createVault( opts, ); } + +/** Validate and own a canonical createVault action without allocating a nonce or calling a wallet/transport. */ +export function buildCreateVault(params: CreateVaultParameters): CanonicalAction { + const action = buildAction(CreateVaultActionSchema, { type: "createVault", ...params, nonce: 0 }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/evmUserModify.ts b/packages/hyperliquid/src/api/exchange/_methods/evmUserModify.ts index a44937c1..b8594f63 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/evmUserModify.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/evmUserModify.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -121,3 +122,9 @@ export function evmUserModify( const action = buildAction(EvmUserModifyActionSchema, { type: "evmUserModify", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical evmUserModify action without allocating a nonce or calling a wallet/transport. */ +export function buildEvmUserModify(params: EvmUserModifyParameters): CanonicalAction { + const action = buildAction(EvmUserModifyActionSchema, { type: "evmUserModify", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/finalizeEvmContract.ts b/packages/hyperliquid/src/api/exchange/_methods/finalizeEvmContract.ts index af71fd9b..1834a9e6 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/finalizeEvmContract.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/finalizeEvmContract.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -139,3 +140,11 @@ export function finalizeEvmContract( const action = buildAction(FinalizeEvmContractActionSchema, { type: "finalizeEvmContract", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical finalizeEvmContract action without allocating a nonce or calling a wallet/transport. */ +export function buildFinalizeEvmContract( + params: FinalizeEvmContractParameters, +): CanonicalAction { + const action = buildAction(FinalizeEvmContractActionSchema, { type: "finalizeEvmContract", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/gossipPriorityBid.ts b/packages/hyperliquid/src/api/exchange/_methods/gossipPriorityBid.ts index 4ab74135..7c42e7b9 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/gossipPriorityBid.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/gossipPriorityBid.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -127,3 +128,11 @@ export function gossipPriorityBid( const action = buildAction(GossipPriorityBidActionSchema, { type: "gossipPriorityBid", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical gossipPriorityBid action without allocating a nonce or calling a wallet/transport. */ +export function buildGossipPriorityBid( + params: GossipPriorityBidParameters, +): CanonicalAction { + const action = buildAction(GossipPriorityBidActionSchema, { type: "gossipPriorityBid", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/hip3LiquidatorTransfer.ts b/packages/hyperliquid/src/api/exchange/_methods/hip3LiquidatorTransfer.ts index 8a8819bc..87ad12dc 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/hip3LiquidatorTransfer.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/hip3LiquidatorTransfer.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -127,3 +128,11 @@ export function hip3LiquidatorTransfer( const action = buildAction(Hip3LiquidatorTransferActionSchema, { type: "hip3LiquidatorTransfer", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical hip3LiquidatorTransfer action without allocating a nonce or calling a wallet/transport. */ +export function buildHip3LiquidatorTransfer( + params: Hip3LiquidatorTransferParameters, +): CanonicalAction { + const action = buildAction(Hip3LiquidatorTransferActionSchema, { type: "hip3LiquidatorTransfer", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/linkStakingUser.ts b/packages/hyperliquid/src/api/exchange/_methods/linkStakingUser.ts index 7b86bc9a..095edf3c 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/linkStakingUser.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/linkStakingUser.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -150,3 +151,11 @@ export function linkStakingUser( const action = buildAction(LinkStakingUserActionSchema, { type: "linkStakingUser", ...params }, opts); return executeUserSignedAction(config, action, LinkStakingUserTypes, opts); } + +/** Validate and own a canonical linkStakingUser action without allocating a nonce or calling a wallet/transport. */ +export function buildLinkStakingUser( + params: LinkStakingUserParameters, +): CanonicalAction { + const action = buildAction(LinkStakingUserActionSchema, { type: "linkStakingUser", ...params }); + return canonicalAction(action, { kind: "user", types: LinkStakingUserTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/modify.ts b/packages/hyperliquid/src/api/exchange/_methods/modify.ts index e3fe8efd..30246c5e 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/modify.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/modify.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -186,3 +187,9 @@ export function modify( const action = buildAction(ModifyActionSchema, { type: "modify", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical modify action without allocating a nonce or calling a wallet/transport. */ +export function buildModify(params: ModifyParameters): CanonicalAction { + const action = buildAction(ModifyActionSchema, { type: "modify", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/noop.ts b/packages/hyperliquid/src/api/exchange/_methods/noop.ts index 94aed5d6..f87b865d 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/noop.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/noop.ts @@ -1,3 +1,5 @@ +import { parse } from "../../../_base.ts"; +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -119,3 +121,9 @@ export function noop(config: ExchangeConfig, params: NoopParameters, opts?: Noop const action = buildAction(NoopActionSchema, { type: "noop" }, opts); return executeL1Action({ ...config, nonceManager: () => params.nonce }, action, opts); } + +/** Validate and own a canonical noop action without allocating a nonce or calling a wallet/transport. */ +export function buildNoop(params: NoopParameters): CanonicalAction { + const action = buildAction(NoopActionSchema, { type: "noop" }); + return canonicalAction(action, { kind: "l1", nonce: parse(UnsignedInteger, params.nonce) }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/order.ts b/packages/hyperliquid/src/api/exchange/_methods/order.ts index 8f0ef9c1..63c60f50 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/order.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/order.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -255,3 +256,9 @@ export function order( const action = buildAction(OrderActionSchema, { type: "order", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical order action without allocating a nonce or calling a wallet/transport. */ +export function buildOrder(params: OrderParameters): CanonicalAction { + const action = buildAction(OrderActionSchema, { type: "order", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/outcomeDeploy.ts b/packages/hyperliquid/src/api/exchange/_methods/outcomeDeploy.ts index 7fdb1afa..978556ef 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/outcomeDeploy.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/outcomeDeploy.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; import { Address, Hex, UnsignedDecimal, UnsignedInteger } from "../../_schemas.ts"; @@ -193,3 +194,9 @@ export function outcomeDeploy( opts, ); } + +/** Validate and own a canonical outcomeDeploy action without allocating a nonce or calling a wallet/transport. */ +export function buildOutcomeDeploy(params: OutcomeDeployParameters): CanonicalAction { + const action = buildAction(OutcomeDeployActionSchema, { type: "outcomeDeploy", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/perpDeploy.ts b/packages/hyperliquid/src/api/exchange/_methods/perpDeploy.ts index b132afb5..44a341af 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/perpDeploy.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/perpDeploy.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -485,3 +486,9 @@ export function perpDeploy( const action = buildAction(PerpDeployActionSchema, { type: "perpDeploy", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical perpDeploy action without allocating a nonce or calling a wallet/transport. */ +export function buildPerpDeploy(params: PerpDeployParameters): CanonicalAction { + const action = buildAction(PerpDeployActionSchema, { type: "perpDeploy", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/registerReferrer.ts b/packages/hyperliquid/src/api/exchange/_methods/registerReferrer.ts index 42e3ab38..3219d75a 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/registerReferrer.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/registerReferrer.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -117,3 +118,11 @@ export function registerReferrer( const action = buildAction(RegisterReferrerActionSchema, { type: "registerReferrer", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical registerReferrer action without allocating a nonce or calling a wallet/transport. */ +export function buildRegisterReferrer( + params: RegisterReferrerParameters, +): CanonicalAction { + const action = buildAction(RegisterReferrerActionSchema, { type: "registerReferrer", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/reserveRequestWeight.ts b/packages/hyperliquid/src/api/exchange/_methods/reserveRequestWeight.ts index adc2d571..a3b2ce06 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/reserveRequestWeight.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/reserveRequestWeight.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -123,3 +124,11 @@ export function reserveRequestWeight( const action = buildAction(ReserveRequestWeightActionSchema, { type: "reserveRequestWeight", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical reserveRequestWeight action without allocating a nonce or calling a wallet/transport. */ +export function buildReserveRequestWeight( + params: ReserveRequestWeightParameters, +): CanonicalAction { + const action = buildAction(ReserveRequestWeightActionSchema, { type: "reserveRequestWeight", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/scheduleCancel.ts b/packages/hyperliquid/src/api/exchange/_methods/scheduleCancel.ts index d9da0fcf..6c2b235d 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/scheduleCancel.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/scheduleCancel.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -177,3 +178,12 @@ export function scheduleCancel( const action = buildAction(ScheduleCancelActionSchema, actionInput, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical scheduleCancel action without allocating a nonce or calling a wallet/transport. */ +export function buildScheduleCancel(params?: ScheduleCancelParameters): CanonicalAction { + const actionInput: Record = { type: "scheduleCancel", ...params }; + if (actionInput.time === null) delete actionInput.time; + if (actionInput.time !== undefined) parse(ScheduleCancelTimeSchema, actionInput.time); + const action = buildAction(ScheduleCancelActionSchema, actionInput); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/sendAsset.ts b/packages/hyperliquid/src/api/exchange/_methods/sendAsset.ts index ade37d0c..d7b91ed5 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/sendAsset.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/sendAsset.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -158,3 +159,9 @@ export function sendAsset( const action = buildAction(SendAssetActionSchema, { type: "sendAsset", ...params }, opts); return executeUserSignedAction(config, action, SendAssetTypes, opts); } + +/** Validate and own a canonical sendAsset action without allocating a nonce or calling a wallet/transport. */ +export function buildSendAsset(params: SendAssetParameters): CanonicalAction { + const action = buildAction(SendAssetActionSchema, { type: "sendAsset", ...params }); + return canonicalAction(action, { kind: "user", types: SendAssetTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/sendToEvmWithData.ts b/packages/hyperliquid/src/api/exchange/_methods/sendToEvmWithData.ts index 5ffb4541..4d85705e 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/sendToEvmWithData.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/sendToEvmWithData.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -166,3 +167,11 @@ export function sendToEvmWithData( const action = buildAction(SendToEvmWithDataActionSchema, { type: "sendToEvmWithData", ...params }, opts); return executeUserSignedAction(config, action, SendToEvmWithDataTypes, opts); } + +/** Validate and own a canonical sendToEvmWithData action without allocating a nonce or calling a wallet/transport. */ +export function buildSendToEvmWithData( + params: SendToEvmWithDataParameters, +): CanonicalAction { + const action = buildAction(SendToEvmWithDataActionSchema, { type: "sendToEvmWithData", ...params }); + return canonicalAction(action, { kind: "user", types: SendToEvmWithDataTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/setDisplayName.ts b/packages/hyperliquid/src/api/exchange/_methods/setDisplayName.ts index 9d577f21..ff6b7653 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/setDisplayName.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/setDisplayName.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -120,3 +121,9 @@ export function setDisplayName( const action = buildAction(SetDisplayNameActionSchema, { type: "setDisplayName", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical setDisplayName action without allocating a nonce or calling a wallet/transport. */ +export function buildSetDisplayName(params: SetDisplayNameParameters): CanonicalAction { + const action = buildAction(SetDisplayNameActionSchema, { type: "setDisplayName", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/setReferrer.ts b/packages/hyperliquid/src/api/exchange/_methods/setReferrer.ts index ea13344b..9ff75f8d 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/setReferrer.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/setReferrer.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -117,3 +118,9 @@ export function setReferrer( const action = buildAction(SetReferrerActionSchema, { type: "setReferrer", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical setReferrer action without allocating a nonce or calling a wallet/transport. */ +export function buildSetReferrer(params: SetReferrerParameters): CanonicalAction { + const action = buildAction(SetReferrerActionSchema, { type: "setReferrer", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/spotDeploy.ts b/packages/hyperliquid/src/api/exchange/_methods/spotDeploy.ts index 40abd8e4..a59c9c7d 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/spotDeploy.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/spotDeploy.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -354,3 +355,9 @@ export function spotDeploy( const action = buildAction(SpotDeployActionSchema, { type: "spotDeploy", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical spotDeploy action without allocating a nonce or calling a wallet/transport. */ +export function buildSpotDeploy(params: SpotDeployParameters): CanonicalAction { + const action = buildAction(SpotDeployActionSchema, { type: "spotDeploy", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/spotSend.ts b/packages/hyperliquid/src/api/exchange/_methods/spotSend.ts index 42e1e061..99b88ceb 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/spotSend.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/spotSend.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -142,3 +143,9 @@ export function spotSend( const action = buildAction(SpotSendActionSchema, { type: "spotSend", ...params }, opts); return executeUserSignedAction(config, action, SpotSendTypes, opts); } + +/** Validate and own a canonical spotSend action without allocating a nonce or calling a wallet/transport. */ +export function buildSpotSend(params: SpotSendParameters): CanonicalAction { + const action = buildAction(SpotSendActionSchema, { type: "spotSend", ...params }); + return canonicalAction(action, { kind: "user", types: SpotSendTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/spotUser.ts b/packages/hyperliquid/src/api/exchange/_methods/spotUser.ts index 385025df..6b4d0e15 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/spotUser.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/spotUser.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -120,3 +121,9 @@ export function spotUser( const action = buildAction(SpotUserActionSchema, { type: "spotUser", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical spotUser action without allocating a nonce or calling a wallet/transport. */ +export function buildSpotUser(params: SpotUserParameters): CanonicalAction { + const action = buildAction(SpotUserActionSchema, { type: "spotUser", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/stakingDeposit.ts b/packages/hyperliquid/src/api/exchange/_methods/stakingDeposit.ts index 3c314180..b21293a3 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/stakingDeposit.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/stakingDeposit.ts @@ -52,3 +52,5 @@ export function stakingDeposit( ): Promise { return cDeposit(config, params, opts); } + +export { buildCDeposit as buildStakingDeposit } from "./cDeposit.ts"; diff --git a/packages/hyperliquid/src/api/exchange/_methods/stakingLinkDisableTradingUser.ts b/packages/hyperliquid/src/api/exchange/_methods/stakingLinkDisableTradingUser.ts index b6a64b1a..4d1ff042 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/stakingLinkDisableTradingUser.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/stakingLinkDisableTradingUser.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -151,3 +152,14 @@ export function stakingLinkDisableTradingUser( ); return executeUserSignedAction(config, action, StakingLinkDisableTradingUserTypes, opts); } + +/** Validate and own a canonical stakingLinkDisableTradingUser action without allocating a nonce or calling a wallet/transport. */ +export function buildStakingLinkDisableTradingUser( + params: StakingLinkDisableTradingUserParameters, +): CanonicalAction { + const action = buildAction(StakingLinkDisableTradingUserActionSchema, { + type: "stakingLinkDisableTradingUser", + ...params, + }); + return canonicalAction(action, { kind: "user", types: StakingLinkDisableTradingUserTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/stakingWithdraw.ts b/packages/hyperliquid/src/api/exchange/_methods/stakingWithdraw.ts index ee6ead33..405de510 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/stakingWithdraw.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/stakingWithdraw.ts @@ -57,3 +57,5 @@ export function stakingWithdraw( ): Promise { return cWithdraw(config, params, opts); } + +export { buildCWithdraw as buildStakingWithdraw } from "./cWithdraw.ts"; diff --git a/packages/hyperliquid/src/api/exchange/_methods/subAccountModify.ts b/packages/hyperliquid/src/api/exchange/_methods/subAccountModify.ts index c58e8c04..5fcf0519 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/subAccountModify.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/subAccountModify.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -120,3 +121,11 @@ export function subAccountModify( const action = buildAction(SubAccountModifyActionSchema, { type: "subAccountModify", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical subAccountModify action without allocating a nonce or calling a wallet/transport. */ +export function buildSubAccountModify( + params: SubAccountModifyParameters, +): CanonicalAction { + const action = buildAction(SubAccountModifyActionSchema, { type: "subAccountModify", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/subAccountSpotTransfer.ts b/packages/hyperliquid/src/api/exchange/_methods/subAccountSpotTransfer.ts index 9d801e85..da82aaab 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/subAccountSpotTransfer.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/subAccountSpotTransfer.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -126,3 +127,11 @@ export function subAccountSpotTransfer( const action = buildAction(SubAccountSpotTransferActionSchema, { type: "subAccountSpotTransfer", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical subAccountSpotTransfer action without allocating a nonce or calling a wallet/transport. */ +export function buildSubAccountSpotTransfer( + params: SubAccountSpotTransferParameters, +): CanonicalAction { + const action = buildAction(SubAccountSpotTransferActionSchema, { type: "subAccountSpotTransfer", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/subAccountTransfer.ts b/packages/hyperliquid/src/api/exchange/_methods/subAccountTransfer.ts index 03935c28..50d5c0a4 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/subAccountTransfer.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/subAccountTransfer.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -123,3 +124,11 @@ export function subAccountTransfer( const action = buildAction(SubAccountTransferActionSchema, { type: "subAccountTransfer", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical subAccountTransfer action without allocating a nonce or calling a wallet/transport. */ +export function buildSubAccountTransfer( + params: SubAccountTransferParameters, +): CanonicalAction { + const action = buildAction(SubAccountTransferActionSchema, { type: "subAccountTransfer", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/tokenDelegate.ts b/packages/hyperliquid/src/api/exchange/_methods/tokenDelegate.ts index 6adbfc98..bf3c855e 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/tokenDelegate.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/tokenDelegate.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -146,3 +147,9 @@ export function tokenDelegate( const action = buildAction(TokenDelegateActionSchema, { type: "tokenDelegate", ...params }, opts); return executeUserSignedAction(config, action, TokenDelegateTypes, opts); } + +/** Validate and own a canonical tokenDelegate action without allocating a nonce or calling a wallet/transport. */ +export function buildTokenDelegate(params: TokenDelegateParameters): CanonicalAction { + const action = buildAction(TokenDelegateActionSchema, { type: "tokenDelegate", ...params }); + return canonicalAction(action, { kind: "user", types: TokenDelegateTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/topUpIsolatedOnlyMargin.ts b/packages/hyperliquid/src/api/exchange/_methods/topUpIsolatedOnlyMargin.ts index 4fd654d3..f9588eef 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/topUpIsolatedOnlyMargin.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/topUpIsolatedOnlyMargin.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -129,3 +130,11 @@ export function topUpIsolatedOnlyMargin( const action = buildAction(TopUpIsolatedOnlyMarginActionSchema, { type: "topUpIsolatedOnlyMargin", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical topUpIsolatedOnlyMargin action without allocating a nonce or calling a wallet/transport. */ +export function buildTopUpIsolatedOnlyMargin( + params: TopUpIsolatedOnlyMarginParameters, +): CanonicalAction { + const action = buildAction(TopUpIsolatedOnlyMarginActionSchema, { type: "topUpIsolatedOnlyMargin", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/twapCancel.ts b/packages/hyperliquid/src/api/exchange/_methods/twapCancel.ts index 1cfa9c47..e9dfea42 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/twapCancel.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/twapCancel.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -129,3 +130,9 @@ export function twapCancel( const action = buildAction(TwapCancelActionSchema, { type: "twapCancel", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical twapCancel action without allocating a nonce or calling a wallet/transport. */ +export function buildTwapCancel(params: TwapCancelParameters): CanonicalAction { + const action = buildAction(TwapCancelActionSchema, { type: "twapCancel", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/twapOrder.ts b/packages/hyperliquid/src/api/exchange/_methods/twapOrder.ts index b8535d7f..bdd4e915 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/twapOrder.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/twapOrder.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -168,3 +169,9 @@ export function twapOrder( const action = buildAction(TwapOrderActionSchema, { type: "twapOrder", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical twapOrder action without allocating a nonce or calling a wallet/transport. */ +export function buildTwapOrder(params: TwapOrderParameters): CanonicalAction { + const action = buildAction(TwapOrderActionSchema, { type: "twapOrder", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/updateIsolatedMargin.ts b/packages/hyperliquid/src/api/exchange/_methods/updateIsolatedMargin.ts index 9acdc828..8acf6d45 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/updateIsolatedMargin.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/updateIsolatedMargin.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -129,3 +130,11 @@ export function updateIsolatedMargin( const action = buildAction(UpdateIsolatedMarginActionSchema, { type: "updateIsolatedMargin", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical updateIsolatedMargin action without allocating a nonce or calling a wallet/transport. */ +export function buildUpdateIsolatedMargin( + params: UpdateIsolatedMarginParameters, +): CanonicalAction { + const action = buildAction(UpdateIsolatedMarginActionSchema, { type: "updateIsolatedMargin", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/updateLeverage.ts b/packages/hyperliquid/src/api/exchange/_methods/updateLeverage.ts index 254c80e0..9331de5c 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/updateLeverage.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/updateLeverage.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -129,3 +130,9 @@ export function updateLeverage( const action = buildAction(UpdateLeverageActionSchema, { type: "updateLeverage", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical updateLeverage action without allocating a nonce or calling a wallet/transport. */ +export function buildUpdateLeverage(params: UpdateLeverageParameters): CanonicalAction { + const action = buildAction(UpdateLeverageActionSchema, { type: "updateLeverage", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/usdClassTransfer.ts b/packages/hyperliquid/src/api/exchange/_methods/usdClassTransfer.ts index 55379c68..98b26445 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/usdClassTransfer.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/usdClassTransfer.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -150,3 +151,11 @@ export function usdClassTransfer( const action = buildAction(UsdClassTransferActionSchema, { type: "usdClassTransfer", ...params }, opts); return executeUserSignedAction(config, action, UsdClassTransferTypes, opts); } + +/** Validate and own a canonical usdClassTransfer action without allocating a nonce or calling a wallet/transport. */ +export function buildUsdClassTransfer( + params: UsdClassTransferParameters, +): CanonicalAction { + const action = buildAction(UsdClassTransferActionSchema, { type: "usdClassTransfer", ...params }); + return canonicalAction(action, { kind: "user", types: UsdClassTransferTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/usdSend.ts b/packages/hyperliquid/src/api/exchange/_methods/usdSend.ts index 2eae15d9..e7665c5c 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/usdSend.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/usdSend.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -138,3 +139,9 @@ export function usdSend( const action = buildAction(UsdSendActionSchema, { type: "usdSend", ...params }, opts); return executeUserSignedAction(config, action, UsdSendTypes, opts); } + +/** Validate and own a canonical usdSend action without allocating a nonce or calling a wallet/transport. */ +export function buildUsdSend(params: UsdSendParameters): CanonicalAction { + const action = buildAction(UsdSendActionSchema, { type: "usdSend", ...params }); + return canonicalAction(action, { kind: "user", types: UsdSendTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/userDexAbstraction.ts b/packages/hyperliquid/src/api/exchange/_methods/userDexAbstraction.ts index 0569defb..72d1f155 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/userDexAbstraction.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/userDexAbstraction.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -144,3 +145,11 @@ export function userDexAbstraction( const action = buildAction(UserDexAbstractionActionSchema, { type: "userDexAbstraction", ...params }, opts); return executeUserSignedAction(config, action, UserDexAbstractionTypes, opts); } + +/** Validate and own a canonical userDexAbstraction action without allocating a nonce or calling a wallet/transport. */ +export function buildUserDexAbstraction( + params: UserDexAbstractionParameters, +): CanonicalAction { + const action = buildAction(UserDexAbstractionActionSchema, { type: "userDexAbstraction", ...params }); + return canonicalAction(action, { kind: "user", types: UserDexAbstractionTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/userOutcome.ts b/packages/hyperliquid/src/api/exchange/_methods/userOutcome.ts index 67fac7e8..ecb1539f 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/userOutcome.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/userOutcome.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -182,3 +183,9 @@ export function userOutcome( const action = buildAction(UserOutcomeActionSchema, { type: "userOutcome", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical userOutcome action without allocating a nonce or calling a wallet/transport. */ +export function buildUserOutcome(params: UserOutcomeParameters): CanonicalAction { + const action = buildAction(UserOutcomeActionSchema, { type: "userOutcome", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/userPortfolioMargin.ts b/packages/hyperliquid/src/api/exchange/_methods/userPortfolioMargin.ts index a283e78e..3213d0b6 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/userPortfolioMargin.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/userPortfolioMargin.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -142,3 +143,11 @@ export function userPortfolioMargin( const action = buildAction(UserPortfolioMarginActionSchema, { type: "userPortfolioMargin", ...params }, opts); return executeUserSignedAction(config, action, UserPortfolioMarginTypes, opts); } + +/** Validate and own a canonical userPortfolioMargin action without allocating a nonce or calling a wallet/transport. */ +export function buildUserPortfolioMargin( + params: UserPortfolioMarginParameters, +): CanonicalAction { + const action = buildAction(UserPortfolioMarginActionSchema, { type: "userPortfolioMargin", ...params }); + return canonicalAction(action, { kind: "user", types: UserPortfolioMarginTypes }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/userSetAbstraction.ts b/packages/hyperliquid/src/api/exchange/_methods/userSetAbstraction.ts index c6dba670..69281a40 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/userSetAbstraction.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/userSetAbstraction.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -157,3 +158,11 @@ export function userSetAbstraction( const action = buildAction(UserSetAbstractionActionSchema, { type: "userSetAbstraction", ...params }, opts); return executeUserSignedAction(config, action, UserSetAbstractionTypes, { ...opts, toMultiSigPayloadAction }); } + +/** Validate and own a canonical userSetAbstraction action without allocating a nonce or calling a wallet/transport. */ +export function buildUserSetAbstraction( + params: UserSetAbstractionParameters, +): CanonicalAction { + const action = buildAction(UserSetAbstractionActionSchema, { type: "userSetAbstraction", ...params }); + return canonicalAction(action, { kind: "user", types: UserSetAbstractionTypes, toMultiSigPayloadAction }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/validatorAction.ts b/packages/hyperliquid/src/api/exchange/_methods/validatorAction.ts index 7ebb2ca4..f9ad81e1 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/validatorAction.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/validatorAction.ts @@ -63,3 +63,5 @@ export function validatorAction( ): Promise { return cValidatorAction(config, params, opts); } + +export { buildCValidatorAction as buildValidatorAction } from "./cValidatorAction.ts"; diff --git a/packages/hyperliquid/src/api/exchange/_methods/validatorL1Stream.ts b/packages/hyperliquid/src/api/exchange/_methods/validatorL1Stream.ts index 3e6769cd..0c56d9ac 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/validatorL1Stream.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/validatorL1Stream.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -121,3 +122,11 @@ export function validatorL1Stream( const action = buildAction(ValidatorL1StreamActionSchema, { type: "validatorL1Stream", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical validatorL1Stream action without allocating a nonce or calling a wallet/transport. */ +export function buildValidatorL1Stream( + params: ValidatorL1StreamParameters, +): CanonicalAction { + const action = buildAction(ValidatorL1StreamActionSchema, { type: "validatorL1Stream", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/validatorSignerAction.ts b/packages/hyperliquid/src/api/exchange/_methods/validatorSignerAction.ts index 766f0d12..1e1ff354 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/validatorSignerAction.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/validatorSignerAction.ts @@ -69,3 +69,5 @@ export function validatorSignerAction( ): Promise { return cSignerAction(config, params, opts); } + +export { buildCSignerAction as buildValidatorSignerAction } from "./cSignerAction.ts"; diff --git a/packages/hyperliquid/src/api/exchange/_methods/vaultDistribute.ts b/packages/hyperliquid/src/api/exchange/_methods/vaultDistribute.ts index a0aecb14..60292828 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/vaultDistribute.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/vaultDistribute.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -123,3 +124,11 @@ export function vaultDistribute( const action = buildAction(VaultDistributeActionSchema, { type: "vaultDistribute", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical vaultDistribute action without allocating a nonce or calling a wallet/transport. */ +export function buildVaultDistribute( + params: VaultDistributeParameters, +): CanonicalAction { + const action = buildAction(VaultDistributeActionSchema, { type: "vaultDistribute", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/vaultModify.ts b/packages/hyperliquid/src/api/exchange/_methods/vaultModify.ts index a75ee905..39e03a97 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/vaultModify.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/vaultModify.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -123,3 +124,9 @@ export function vaultModify( const action = buildAction(VaultModifyActionSchema, { type: "vaultModify", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical vaultModify action without allocating a nonce or calling a wallet/transport. */ +export function buildVaultModify(params: VaultModifyParameters): CanonicalAction { + const action = buildAction(VaultModifyActionSchema, { type: "vaultModify", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/vaultTransfer.ts b/packages/hyperliquid/src/api/exchange/_methods/vaultTransfer.ts index c16034c6..14302a52 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/vaultTransfer.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/vaultTransfer.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -127,3 +128,9 @@ export function vaultTransfer( const action = buildAction(VaultTransferActionSchema, { type: "vaultTransfer", ...params }, opts); return executeL1Action(config, action, opts); } + +/** Validate and own a canonical vaultTransfer action without allocating a nonce or calling a wallet/transport. */ +export function buildVaultTransfer(params: VaultTransferParameters): CanonicalAction { + const action = buildAction(VaultTransferActionSchema, { type: "vaultTransfer", ...params }); + return canonicalAction(action, { kind: "l1" }); +} diff --git a/packages/hyperliquid/src/api/exchange/_methods/withdraw.ts b/packages/hyperliquid/src/api/exchange/_methods/withdraw.ts index 5b2b6552..c0a21b97 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/withdraw.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/withdraw.ts @@ -58,3 +58,5 @@ export function withdraw( ): Promise { return withdraw3(config, params, opts); } + +export { buildWithdraw3 as buildWithdraw } from "./withdraw3.ts"; diff --git a/packages/hyperliquid/src/api/exchange/_methods/withdraw3.ts b/packages/hyperliquid/src/api/exchange/_methods/withdraw3.ts index db54ec1e..ab48e26b 100644 --- a/packages/hyperliquid/src/api/exchange/_methods/withdraw3.ts +++ b/packages/hyperliquid/src/api/exchange/_methods/withdraw3.ts @@ -1,3 +1,4 @@ +import { canonicalAction, type CanonicalAction } from "../../../actions/_canonical.ts"; import * as v from "valibot"; // ============================================================ @@ -140,3 +141,9 @@ export function withdraw3( const action = buildAction(Withdraw3ActionSchema, { type: "withdraw3", ...params }, opts); return executeUserSignedAction(config, action, Withdraw3Types, opts); } + +/** Validate and own a canonical withdraw3 action without allocating a nonce or calling a wallet/transport. */ +export function buildWithdraw3(params: Withdraw3Parameters): CanonicalAction { + const action = buildAction(Withdraw3ActionSchema, { type: "withdraw3", ...params }); + return canonicalAction(action, { kind: "user", types: Withdraw3Types }); +} diff --git a/packages/hyperliquid/src/api/exchange/client.ts b/packages/hyperliquid/src/api/exchange/client.ts index d29224bf..078aa727 100644 --- a/packages/hyperliquid/src/api/exchange/client.ts +++ b/packages/hyperliquid/src/api/exchange/client.ts @@ -1,3 +1,11 @@ +import { + executeAction, + signAction, + submitAction, + type CanonicalAction, + type SignedAction, + type ActionOptions, +} from "../../actions/execution.ts"; /** * Client for the Hyperliquid Exchange API endpoint. * @module @@ -579,6 +587,39 @@ export class ExchangeClient(action: CanonicalAction, options?: ActionOptions): Promise> { + return signAction(this.config, action, options); + } + + /** + * Submit an owned signed action without signing again. + * @param signed Signed request produced by signAction. + * @param options Cancellation signal for submission. + * @return The action's response. + * @throws {HyperliquidError} When request ownership, network, is invalid. + */ + submit(signed: SignedAction, options?: { signal?: AbortSignal }): Promise { + return submitAction(this.config, signed, options); + } + + /** + * Execute a reusable validated action through the coordinated signing path. + * @param action Action produced by an SDK builder. + * @param options Signing options and cancellation signal. + * @return The action's response. + * @throws {HyperliquidError} When action ownership is invalid. + */ + execute(action: CanonicalAction, options?: ActionOptions): Promise { + return executeAction(this.config, action, options); + } + /** * Warm the signing path so the first action signs at steady-state speed. * @@ -3374,3 +3415,5 @@ export type { OutcomeDeploySuccessResponse, OutcomeDeployOptions, } from "./_methods/outcomeDeploy.ts"; + +export type { CanonicalAction, SignedAction, ActionOptions } from "../../actions/execution.ts"; diff --git a/packages/hyperliquid/src/signing/_keccak.ts b/packages/hyperliquid/src/signing/_keccak.ts index f2dd95f1..974247b9 100644 --- a/packages/hyperliquid/src/signing/_keccak.ts +++ b/packages/hyperliquid/src/signing/_keccak.ts @@ -28,8 +28,13 @@ interface WasmKeccak { init(): void; update(data: Uint8Array): void; digest(outputType: "binary"): Uint8Array; + save?(): Uint8Array; + load?(state: Uint8Array): void; } +/** State resumption is optional and independently checked before it is used. */ +type CheckpointHasher = WasmKeccak & Required>; + /** keccak-256 of the empty input — the known-answer check a WASM build must pass before it is trusted. */ const KECCAK256_EMPTY = "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; @@ -88,12 +93,42 @@ let loadPromise: Promise | undefined; */ let wasmHasher: WasmKeccak | undefined; +/** The loaded provider only when its save/load round trip also passed conformance. */ +let checkpointHasher: CheckpointHasher | undefined; + +/** Check a partial block, a crossed block boundary, and repeated loads of the same snapshot. */ +function validateCheckpointHasher(hasher: WasmKeccak): hasher is CheckpointHasher { + if (typeof hasher.save !== "function" || typeof hasher.load !== "function") return false; + try { + const prefix = Uint8Array.from({ length: 137 }, (_, i) => i); + const tail = new Uint8Array(29).fill(0xa5); + const expected = keccak_256.create().update(prefix).update(tail).digest(); + hasher.init(); + hasher.update(prefix); + const saved = hasher.save(); + for (let i = 0; i < 2; i++) { + hasher.init(); + hasher.update(tail); + hasher.digest("binary"); + hasher.load(saved); + hasher.update(tail); + if (bytesToHex(hasher.digest("binary")) !== bytesToHex(expected)) return false; + } + return true; + } catch { + return false; + } +} + /** Starts the one-time background load (no-op once started); the dispatch keeps using noble until it settles. */ function kickWasmLoad(): void { if (loadPromise !== undefined) return; loadPromise = keccakLoader().then( (hasher) => { - if (validateWasmKeccak(hasher)) wasmHasher = hasher; + if (validateWasmKeccak(hasher)) { + wasmHasher = hasher; + if (validateCheckpointHasher(hasher)) checkpointHasher = hasher; + } }, () => { // A rejected loader leaves the dispatch on noble for the life of the process. @@ -123,6 +158,43 @@ export function keccak256(data: Uint8Array): Uint8Array { return keccak_256(data); } +/** + * Checkpoint an SDK-owned immutable prefix; hash a fresh tail on every invocation. + * The prefix must remain unchanged for the closure's lifetime. Only the immutable L1 action + * cache uses this; mutable public signing inputs always take the complete hash path. + * + * Initialization is lazy. A noble checkpoint upgrades to WASM when it becomes ready, and a + * WASM snapshot is tied to its exact provider instance so a loader reset cannot reuse it on + * another build. Each invocation clones/loads the unfinalized state before adding the tail. + * No finalized hash, nonce, metadata, or signature is cached. + * + * @param prefix The immutable preimage prefix, retained by the returned closure. + * @return A synchronous hasher for prefix followed by its supplied tail. + */ +export function createKeccakPrefix(prefix: Uint8Array): (tail: Uint8Array) => Uint8Array { + let nobleState: ReturnType | undefined; + let provider: CheckpointHasher | undefined; + let saved: Uint8Array | undefined; + return (tail: Uint8Array): Uint8Array => { + const hasher = checkpointHasher; + if (hasher !== undefined) { + if (provider !== hasher) { + hasher.init(); + hasher.update(prefix); + saved = hasher.save(); + provider = hasher; + nobleState = undefined; + } + hasher.load(saved!); + hasher.update(tail); + return hasher.digest("binary"); + } + kickWasmLoad(); + nobleState ??= keccak_256.create().update(prefix); + return nobleState.clone().update(tail).digest(); + }; +} + /** * Starts the background load of the optional WASM keccak (`hash-wasm`) if it has not started yet, * and resolves once it has settled. @@ -160,4 +232,5 @@ export function _setKeccakLoaderForTests(loader: (() => Promise Uint8Array }>(); + +/** Below two Keccak blocks, restoring state costs more than rehashing with WASM. */ +const CHECKPOINT_MIN_BYTES = 272; + +/** + * Register only a recursively copied/frozen plain payload owned by an SDK builder. + * A shallow Object.freeze on a caller's object does not establish this invariant. + * No serialization is paid until the action is actually signed. + */ +export function registerImmutableL1Action(action: Readonly>): void { + actions.set(action, {}); +} + +/** + * Write an action at the beginning of a reset writer, or return a prefix hasher and leave + * the writer empty for the fresh tail. Only a registered immutable payload reused after + * its first hash can take the checkpoint path; small actions continue reusing bytes. + * Ordinary public signing inputs are encoded on every call. Nonce, vault, expiry, digest, + * and signature remain fresh on every execution. + * + * @param writer The reset preimage writer. + * @param action The action to hash. + * @return A checkpoint hasher when only the metadata tail should be written. + */ +export function writeL1Action( + writer: MsgpackWriter, + action: Record | unknown[], +): ((tail: Uint8Array) => Uint8Array) | undefined { + const entry = actions.get(action); + if (entry?.bytes !== undefined) { + if (entry.bytes.length >= CHECKPOINT_MIN_BYTES) { + return (entry.hashTail ??= createKeccakPrefix(entry.bytes)); + } + writer.raw(entry.bytes); + return undefined; + } + writer.valueL1(action as L1Value); + if (entry) entry.bytes = writer.view().slice(); + return undefined; +} diff --git a/packages/hyperliquid/src/transport/_wire.ts b/packages/hyperliquid/src/transport/_wire.ts new file mode 100644 index 00000000..7983cad8 --- /dev/null +++ b/packages/hyperliquid/src/transport/_wire.ts @@ -0,0 +1,96 @@ +/** JSON fragment reuse for SDK-owned bulk actions. Never inspect unregistered caller inputs. @module */ + +interface ActionEntry { + json?: string; +} +const actions = new WeakMap(); +const requests = new WeakMap(); +const keys = new Set(["action", "signature", "nonce", "vaultAddress", "expiresAfter"]); + +/** + * Register only a recursively copied/frozen builder payload. Small actions retain native + * JSON.stringify; the descriptor checks pay off only when an action has a bulk array. + * @param action The SDK-owned immutable action. + */ +export function registerImmutableWireAction(action: Readonly>): void { + if (Object.values(action).some((value) => Array.isArray(value) && value.length >= 8)) { + actions.set(action, {}); + } +} + +/** + * Register an envelope constructed by the execution shell, without freezing or changing it. + * The serializer rechecks its descriptors and action identity because prepared requests and + * custom transports may mutate it later. Arbitrary public transport inputs remain unregistered. + * @param request The shell's plain request object. + * @param action The action the shell put into it. + */ +export function registerExchangeWireRequest(request: object, action: unknown): void { + if (action === null || typeof action !== "object") return; + const entry = actions.get(action); + if (entry) { + // The execution shell supplies the fresh, plain r/s/v object returned by the signer. + const signature = Object.getOwnPropertyDescriptor(request, "signature")?.value; + if (signature === null || typeof signature !== "object") return; + requests.set(request, { action, signature, entry }); + } +} + +/** + * Serialize a registered bulk envelope, or decline without invoking any caller getter/toJSON. + * Metadata and signatures are serialized fresh, keeping their nested toJSON key arguments. + * The action fragment is safe to reuse because its entire tree is SDK-owned plain data. + * Prototype toJSON hooks invalidate this optimization, including hooks on nested arrays. + * + * @param payload The exchange request to inspect. + * @return Its exact native JSON form, or undefined to request normal JSON.stringify. + */ +export function exchangeWireJSON(payload: unknown): string | undefined { + if (payload === null || typeof payload !== "object") return undefined; + const record = requests.get(payload); + if (record === undefined) return undefined; + if (Object.getPrototypeOf(payload) !== Object.prototype || "toJSON" in payload || "toJSON" in Array.prototype) + return undefined; + const descriptors = Object.getOwnPropertyDescriptors(payload); + const names = Object.keys(payload); + if ( + names[0] !== "action" || + descriptors.action?.value !== record.action || + descriptors.signature?.value !== record.signature + ) + return undefined; + const signature = record.signature; + if (Object.getPrototypeOf(signature) !== Object.prototype || "toJSON" in signature) return undefined; + for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(signature))) { + // An accessor or an object could run code during serialization that mutates a later + // envelope field. Decline before invoking it, preserving native read order exactly. + if ( + !("value" in descriptor) || + (descriptor.value !== null && typeof descriptor.value === "object") || + typeof descriptor.value === "function" || + typeof descriptor.value === "bigint" + ) + return undefined; + } + const metadata: Record = {}; + for (const name of names) { + const descriptor = descriptors[name]; + if (!keys.has(name) || !("value" in descriptor)) return undefined; + if (name !== "action") { + if ( + name !== "signature" && + descriptor.value !== null && + (typeof descriptor.value === "object" || + typeof descriptor.value === "function" || + typeof descriptor.value === "bigint") + ) + return undefined; + metadata[name] = descriptor.value; + } + } + // Both serializers operate on plain, data-only roots with no inherited toJSON. + // Native JSON still handles escaping, omission, non-finite numbers, and signature fields. + const actionJSON = (record.entry.json ??= JSON.stringify(record.action)); + const metadataJSON = JSON.stringify(metadata); + return metadataJSON === "{}" ? `{"action":${actionJSON}}` : `{"action":${actionJSON},${metadataJSON.slice(1)}`; +} diff --git a/packages/hyperliquid/src/transport/http/mod.ts b/packages/hyperliquid/src/transport/http/mod.ts index cbf44786..f2961cd5 100644 --- a/packages/hyperliquid/src/transport/http/mod.ts +++ b/packages/hyperliquid/src/transport/http/mod.ts @@ -33,6 +33,7 @@ import { type IRequestTransport, TransportError } from "../_base.ts"; import * as abort from "../_abort.ts"; import { redactSignature, UNSERIALIZABLE_REQUEST } from "../_redact.ts"; import { TokenBucketRateLimiter } from "../_rateLimiter.ts"; +import { exchangeWireJSON } from "../_wire.ts"; /** Configuration options for the HTTP transport layer. */ export interface HttpTransportOptions { @@ -397,7 +398,7 @@ export class HttpTransport implements IRequestTransport<"info" | "exchange" | "e try { // --- Serialize ----------------------------------------------------------- - body = JSON.stringify(payload); + body = (endpoint === "exchange" ? exchangeWireJSON(payload) : undefined) ?? JSON.stringify(payload); // --- Rate limiting ------------------------------------------------------- // Opt-in token bucket: the request waits here for its weight. The wait honors caller diff --git a/packages/hyperliquid/src/transport/websocket/_dispatcher.ts b/packages/hyperliquid/src/transport/websocket/_dispatcher.ts index 7359ebfa..ea7c03f7 100644 --- a/packages/hyperliquid/src/transport/websocket/_dispatcher.ts +++ b/packages/hyperliquid/src/transport/websocket/_dispatcher.ts @@ -13,6 +13,7 @@ import { redactSignature, UNSERIALIZABLE_REQUEST } from "../_redact.ts"; import type { HyperliquidEventTarget, PostResponse, SubscribeUnsubscribeResponse } from "./_events.ts"; import { isSubset, normalize, requestToId, specificity } from "./_id.ts"; import type { WebSocketQuota } from "./_quota.ts"; +import { exchangeWireJSON } from "../_wire.ts"; // ============================================================================= // Errors @@ -96,7 +97,9 @@ export interface RequestHint { * snapshot): the dispatcher then skips its own normalize of the subscription subtree and * builds the envelope id by concatenation. Ignored for `post` requests. */ - subscriptionId: string; + subscriptionId?: string; + /** The original exchange payload in the transport-owned {type: "action", payload} envelope. */ + exchangePayload?: unknown; } /** A queued request awaiting its response. */ @@ -358,6 +361,12 @@ export class WebSocketDispatcher { let echo: PendingRequest["echo"]; if ("id" in request) { id = request.id; + if (hint?.exchangePayload !== undefined) { + const json = exchangeWireJSON(hint.exchangePayload); + if (json !== undefined) { + frame = `{"method":"post","id":${id},"request":{"type":"action","payload":${json}}}`; + } + } } else if (hint?.subscriptionId !== undefined) { // The subscription manager hands over the id it already computed; per the hint contract // `payload` is its normalized snapshot, so the normalized envelope is one shallow object diff --git a/packages/hyperliquid/src/transport/websocket/mod.ts b/packages/hyperliquid/src/transport/websocket/mod.ts index 32542588..6ab82626 100644 --- a/packages/hyperliquid/src/transport/websocket/mod.ts +++ b/packages/hyperliquid/src/transport/websocket/mod.ts @@ -236,7 +236,12 @@ export class WebSocketTransport implements IRequestTransport<"info" | "exchange" */ request(endpoint: "info" | "exchange", payload: unknown, signal?: AbortSignal): Promise { const wrapped = { type: endpoint === "exchange" ? "action" : endpoint, payload }; - return this._dispatcher.request("post", wrapped, signal); + return this._dispatcher.request( + "post", + wrapped, + signal, + endpoint === "exchange" ? { exchangePayload: payload } : undefined, + ); } /** diff --git a/packages/hyperliquid/tests/api/exchange/_actions.test.ts b/packages/hyperliquid/tests/api/exchange/_actions.test.ts new file mode 100644 index 00000000..cf5a924f --- /dev/null +++ b/packages/hyperliquid/tests/api/exchange/_actions.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, test } from "bun:test"; +import { privateKeyToAccount } from "viem/accounts"; +import { ExchangeClient } from "../../../src/api/exchange/client.ts"; +import type { ExchangeConfig } from "../../../src/api/exchange/_methods/_base/_config.ts"; +import type { IRequestTransport } from "../../../src/transport/_base.ts"; +import { buildOrder, order, type OrderSuccessResponse } from "@bloxwap/hyperliquid/api/exchange/order"; +import { buildUsdSend, usdSend } from "@bloxwap/hyperliquid/api/exchange/usdSend"; +import { buildApproveAgent, approveAgent } from "@bloxwap/hyperliquid/api/exchange/approveAgent"; +import { buildCreateVault, createVault } from "@bloxwap/hyperliquid/api/exchange/createVault"; +import { buildAgentSendAsset, agentSendAsset } from "@bloxwap/hyperliquid/api/exchange/agentSendAsset"; +import { buildUserSetAbstraction, userSetAbstraction } from "@bloxwap/hyperliquid/api/exchange/userSetAbstraction"; +import { executeAction, signAction, submitAction, type CanonicalAction } from "@bloxwap/hyperliquid/actions/execution"; + +const wallet = privateKeyToAccount(`0x${"11".repeat(32)}`); +const secondWallet = privateKeyToAccount(`0x${"22".repeat(32)}`); +const address = "0x3333333333333333333333333333333333333333" as const; +const input = { orders: [{ a: 0, b: true, p: "30000", s: "0.1", r: false, t: { limit: { tif: "Gtc" as const } } }] }; + +function harness(multi = false): { config: ExchangeConfig; calls: unknown[] } { + const calls: unknown[] = []; + const transport: IRequestTransport = { + isTestnet: true, + request: async (_endpoint: "info" | "exchange", body: unknown): Promise => { + calls.push(body); + return { status: "ok", response: { type: "order", data: { statuses: [{ resting: { oid: 1 } }] } } } as T; + }, + }; + const base = { transport, signatureChainId: "0x1" as const, nonceManager: () => 1_700_000_000_000 }; + return { + config: multi ? { ...base, signers: [wallet, secondWallet], multiSigUser: address } : { ...base, wallet }, + calls, + }; +} + +describe("canonical actions", () => { + test("builders own and freeze nested input without freezing the caller's objects", () => { + const params = structuredClone(input); + const built = buildOrder(params); + params.orders[0].p = "1"; + expect((built.payload.orders as typeof params.orders)[0].p).toBe("30000"); + expect(Object.isFrozen(params.orders)).toBe(false); + expect(Object.isFrozen(built.payload.orders)).toBe(true); + expect(() => { + (built.payload.orders as typeof params.orders)[0].p = "1"; + }).toThrow(); + expect(() => buildOrder({ orders: [] })).toThrow(); + }); + + test("sign consumes one nonce without posting; submit retains the inferred response", async () => { + const { config, calls } = harness(); + let issued = 0; + config.nonceManager = () => 1_700_000_000_000 + issued++; + const client = new ExchangeClient(config); + const action = buildOrder(input); + expect(issued).toBe(0); + const signed = await client.sign(action); + expect(issued).toBe(1); + expect(calls).toHaveLength(0); + expect(Object.isFrozen(signed.action)).toBe(true); + const response: OrderSuccessResponse = await client.submit(signed); + expect(response.status).toBe("ok"); + expect(issued).toBe(1); + await client.execute(action); + expect(issued).toBe(2); + }); + + const cases: { + name: string; + direct: (config: ExchangeConfig) => Promise; + build: () => CanonicalAction; + }[] = [ + { name: "order", direct: (c) => order(c, input), build: () => buildOrder(input) }, + { + name: "user-signed", + direct: (c) => usdSend(c, { destination: address, amount: "1" }), + build: () => buildUsdSend({ destination: address, amount: "1" }), + }, + { + name: "unnamed agent", + direct: (c) => approveAgent(c, { agentAddress: address }), + build: () => buildApproveAgent({ agentAddress: address }), + }, + { + name: "named agent", + direct: (c) => approveAgent(c, { agentAddress: address, agentName: "bot" }), + build: () => buildApproveAgent({ agentAddress: address, agentName: "bot" }), + }, + { + name: "vault nonce", + direct: (c) => + createVault(c, { name: "my vault", description: "test vault description", initialUsd: 100_000_000 }), + build: () => + buildCreateVault({ name: "my vault", description: "test vault description", initialUsd: 100_000_000 }), + }, + { + name: "agent nonce", + direct: (c) => + agentSendAsset(c, { destination: address, sourceDex: "spot", destinationDex: "", token: "USDC", amount: "1" }), + build: () => + buildAgentSendAsset({ + destination: address, + sourceDex: "spot", + destinationDex: "", + token: "USDC", + amount: "1", + }), + }, + { + name: "multi-sig payload transformation", + direct: (c) => userSetAbstraction(c, { user: address, abstraction: "unifiedAccount" }), + build: () => buildUserSetAbstraction({ user: address, abstraction: "unifiedAccount" }), + }, + ]; + for (const multi of [false, true]) + for (const example of cases) { + test(`${example.name}: direct, staged, and reusable paths have identical signatures/wire bodies (${multi ? "multi" : "single"})`, async () => { + const { config, calls } = harness(multi); + await example.direct(config); + const action = example.build(); + const signed = await signAction(config, action); + expect(Object.isFrozen(signed)).toBe(true); + expect(Object.isFrozen(signed.signature)).toBe(true); + expect(Object.isFrozen(signed.action)).toBe(true); + if (!multi && example.name === "order") expect(signed.action).toBe(action.payload); + else expect(signed.action).not.toBe(action.payload); + await submitAction(config, signed); + await executeAction(config, action); + const wire = calls.map((body) => JSON.stringify(body)); + expect(wire[1]).toBe(wire[0]); + expect(wire[2]).toBe(wire[0]); + }); + } + + test("unknown/reconstructed actions and cross-network signed submissions are rejected", async () => { + const { config, calls } = harness(); + await expect(signAction(config, { payload: {} })).rejects.toThrow("SDK builder"); + const signed = await signAction(config, buildOrder(input)); + await expect( + submitAction({ ...config, transport: { ...config.transport, isTestnet: false } }, signed), + ).rejects.toThrow("network mismatch"); + await expect(submitAction(config, JSON.parse(JSON.stringify(signed)))).rejects.toThrow("ownership"); + expect(calls).toHaveLength(0); + }); +}); diff --git a/packages/hyperliquid/tests/perf/README.md b/packages/hyperliquid/tests/perf/README.md index 72180ae0..7dc4763c 100644 --- a/packages/hyperliquid/tests/perf/README.md +++ b/packages/hyperliquid/tests/perf/README.md @@ -165,3 +165,77 @@ the process-wide paced quota, so a post benchmark cannot delay a later subscript Message pacing itself is verified with controlled clocks in `tests/transport/websocket/_quota.test.ts`; it is excluded from CPU timings. Changing this setup changes the suite fingerprint intentionally, so review the suite migration and record a fresh matching-machine baseline. + +## Optimization search: reusable actions + +Three experiments were paired on the initial working tree, including its existing uncommitted optimizations, +and measured on both Bun and Node. The search rotates variant order over 11 samples, varies nonces, +checks byte equality, and measures checkpoint setup separately. It lives outside this suite so the +regression workload and its fingerprint remain unchanged. + +1. **Checkpoint Keccak before the nonce.** An SDK-owned immutable action can retain its unfinalized hash state, + then hash only fresh nonce/vault/expiry bytes. Implemented for reused prefixes of at least 272 bytes: + noble clones its state, and the optional WASM provider saves/loads its state after an independent conformance + check. A provider change recreates the checkpoint. Single-order actions retain full hashing. +2. **Reuse the immutable action's wire JSON.** Implemented for SDK-owned actions with a bulk array of at least + eight items. The execution shell registers its envelopes; HTTP and WebSocket reuse the action fragment while + serializing each signature and metadata afresh. Unknown inputs, altered descriptors, replacement actions or + signatures, and custom `toJSON` behavior fall back to native serialization. Caches are weakly owned. +3. **Turn MessagePack field names into atoms.** Rejected: a single-character ASCII shortcut and a bounded + encoded-key dictionary both slowed the existing writer on both engines. Those candidates remain in the + experiment script, with no production encoder changes. + +Warm execution of a reused action, with a real WASM local signer and mock transports that materialize the +outgoing UTF-8 bytes, measured on Apple M3 Max: + +| Runtime | Orders per action | HTTP before → after | WebSocket before → after | +| --- | ---: | ---: | ---: | +| Bun 1.2.20 | 100 | 102.7 → 54.9 µs | 101.6 → 55.8 µs | +| Bun 1.2.20 | 1,000 | 516.6 → 58.1 µs | 519.5 → 61.7 µs | +| Node 24.6.0 | 100 | 108.7 → 85.1 µs | 108.3 → 84.2 µs | +| Node 24.6.0 | 1,000 | 390.7 → 145.8 µs | 391.8 → 145.1 µs | + +These are microseconds per **complete action**, not per order. They exclude network/server latency and assume +the same builder payload is reused; constructing a new action for every call pays serialization and prefix +hashing again. The original 57-scenario suite also passed its before/after regression comparison. The full +offline test suite passed on Bun 1.4.0; older Bun 1.2.20 lacks the compression streams that its tests require. + +```ts +import { buildOrder } from "@bloxwap/hyperliquid/actions/order"; +import { executeAction } from "@bloxwap/hyperliquid/actions/execution"; + +const action = buildOrder({ orders }); +await executeAction(config, action); // cold serialization/signing +await executeAction(config, action); // reused payload, fresh nonce/signature +``` + +For changed prices or sizes, build a new action. The SDK owns and freezes the copied payload; ordinary or +shallow-frozen caller objects never gain the hash cache. Repeated execution is deliberate submission of another +action, and must match the application's intent; it is not a retry of a previously signed nonce. + +The separate `signAction` / `submitAction` path also preserves the owned payload and its caches. Each sign +allocates a fresh nonce and freezes a fresh signature, while submission uses that signed request. Transformed +user actions and multi-sig wrappers retain their full immutable copy. + +For the separate sign/submit path with a real WASM signer, a reused 1,000-order action over mocked HTTP +measured 1,292.6 → 60.1 µs on Bun and 1,372.3 → 150.5 µs on Node. These timings include signing, immutable +request preparation, transport overhead, and outgoing UTF-8 materialization; network latency is excluded. + +Run the candidate search from the SDK directory with either engine: + +```sh +bun .dev/perf/optimization_search.ts --out /tmp/search-bun.json +node .dev/perf/optimization_search.ts --out /tmp/search-node.json + +# Pair actual implementations with an original package snapshot containing src/, package.json, +# and access to the same dependencies. Both optional accelerators should be installed. +bun .dev/perf/optimization_verify.ts --compare-dir /path/to/original-sdk --out /tmp/verify-bun.json +node .dev/perf/optimization_verify.ts --compare-dir /path/to/original-sdk --out /tmp/verify-node.json + +# Measure the separate sign/submit path alone. +bun .dev/perf/optimization_verify.ts --compare-dir /path/to/original-sdk --filter staged --out /tmp/staged-bun.json +node .dev/perf/optimization_verify.ts --compare-dir /path/to/original-sdk --filter staged --out /tmp/staged-node.json +``` + +The recorded medians, p90s, environment, decisions, and validation are in +[`optimization_summary.json`](../../.dev/perf/results/optimization_summary.json). diff --git a/packages/hyperliquid/tests/signing/keccakPrefix.test.ts b/packages/hyperliquid/tests/signing/keccakPrefix.test.ts new file mode 100644 index 00000000..2bc81015 --- /dev/null +++ b/packages/hyperliquid/tests/signing/keccakPrefix.test.ts @@ -0,0 +1,92 @@ +/** Differential checks for resumable Keccak preimages, including provider transitions. @module */ +import { afterEach, expect, test } from "bun:test"; +import { keccak_256 } from "@noble/hashes/sha3.js"; +import { + createKeccakPrefix, + _setKeccakLoaderForTests, + preloadWasmKeccak, + keccak256, +} from "../../src/signing/_keccak.ts"; +import { createL1ActionHash } from "../../src/signing/_l1.ts"; +import { buildOrder } from "../../src/api/exchange/_methods/order.ts"; + +afterEach(() => _setKeccakLoaderForTests(undefined)); + +function checkPrefixes(): void { + for (const length of [0, 1, 127, 128, 135, 136, 137, 255, 256, 271, 272, 273, 4096, 65536]) { + const prefix = Uint8Array.from({ length }, (_, i) => (i * 13 + length) & 255); + const digest = createKeccakPrefix(prefix); + for (const size of [0, 1, 9, 29, 38, 135, 136, 137]) { + const tail = Uint8Array.from({ length: size }, (_, i) => (i * 17 + size) & 255); + const expected = keccak_256.create().update(prefix).update(tail).digest(); + expect(digest(tail)).toEqual(expected); + keccak256(new Uint8Array(500).fill(size)); + expect(digest(tail)).toEqual(expected); + } + } +} + +test("prefixes match complete Keccak with the optional provider absent", async () => { + _setKeccakLoaderForTests(async () => undefined); + await preloadWasmKeccak(); + checkPrefixes(); +}); + +test("prefixes match complete Keccak with the real optional provider", async () => { + _setKeccakLoaderForTests(undefined); + await preloadWasmKeccak(); + checkPrefixes(); +}); + +test("an existing prefix remains correct across noble/WASM/provider-instance changes", async () => { + const prefix = new Uint8Array(4097).fill(0x41); + const tail = new Uint8Array(38).fill(0x59); + const expected = keccak_256.create().update(prefix).update(tail).digest(); + _setKeccakLoaderForTests(async () => undefined); + const digest = createKeccakPrefix(prefix); + expect(digest(tail)).toEqual(expected); + for (const real of [true, false, true]) { + _setKeccakLoaderForTests(real ? undefined : async () => undefined); + await preloadWasmKeccak(); + expect(digest(tail)).toEqual(expected); + } +}); + +test("broken save/load cannot corrupt a valid one-shot provider", async () => { + let data = new Uint8Array(0); + _setKeccakLoaderForTests(async () => ({ + init: () => { + data = new Uint8Array(0); + }, + update: (bytes) => { + data = Uint8Array.from([...data, ...bytes]); + }, + digest: () => keccak_256(data), + save: () => new Uint8Array(0), + load: () => { + data = new Uint8Array(0); + }, + })); + await preloadWasmKeccak(); + checkPrefixes(); +}); + +test("owned action checkpoints keep metadata fresh after provider changes", async () => { + const input = { a: 0, b: true, p: "30000", s: "0.1", r: false, t: { limit: { tif: "Gtc" as const } } }; + const action = buildOrder({ orders: Array.from({ length: 100 }, () => input) }); + const fresh = structuredClone(action.payload); + for (const real of [false, true, false, true]) { + _setKeccakLoaderForTests(real ? undefined : async () => undefined); + await preloadWasmKeccak(); + for (const nonce of [0, 1, 255, 0xffffffff, 0x100000000, 1_700_000_000_000, Number.MAX_SAFE_INTEGER]) { + for (const vaultAddress of [undefined, "0x3333333333333333333333333333333333333333" as const]) { + for (const expiresAfter of [undefined, 0, nonce + 100]) { + const args = { nonce, vaultAddress, expiresAfter }; + expect(createL1ActionHash({ ...args, action: action.payload })).toBe( + createL1ActionHash({ ...args, action: fresh }), + ); + } + } + } + } +}); diff --git a/packages/hyperliquid/tests/signing/l1Cache.test.ts b/packages/hyperliquid/tests/signing/l1Cache.test.ts new file mode 100644 index 00000000..ade4b72f --- /dev/null +++ b/packages/hyperliquid/tests/signing/l1Cache.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from "bun:test"; +import { buildOrder } from "../../src/api/exchange/_methods/order.ts"; +import { buildCancel } from "../../src/api/exchange/_methods/cancel.ts"; +import { createL1ActionHash, signL1Action } from "../../src/signing/_l1.ts"; +import { signAction } from "../../src/actions/execution.ts"; +import { privateKeyToAccount } from "viem/accounts"; +import { createFastLocalWallet } from "../../src/signing/_fastWallet.ts"; + +const nonce = 1_700_000_000_000; +const vault = "0x3333333333333333333333333333333333333333" as const; +const input = { a: 0, b: true, p: "30000", s: "0.1", r: false, t: { limit: { tif: "Gtc" as const } } }; + +test("immutable byte reuse preserves hashes across batch headers, nonces, vaults, and expiry", () => { + for (const count of [1, 15, 16, 39, 40, 100]) { + const actions = [ + buildOrder({ orders: Array.from({ length: count }, (_, i) => ({ ...input, a: i, p: `${30000 + i}` })) }), + buildCancel({ cancels: Array.from({ length: count }, (_, i) => ({ a: i, o: nonce + i })) }), + ]; + for (const action of actions) { + for (const offset of [0, 1, 1000]) { + for (const vaultAddress of [undefined, vault]) { + for (const expiresAfter of [undefined, nonce + 60_000]) { + const args = { nonce: nonce + offset, vaultAddress, expiresAfter }; + expect(createL1ActionHash({ ...args, action: action.payload })).toBe( + createL1ActionHash({ ...args, action: structuredClone(action.payload) }), + ); + } + } + } + } + } +}); + +test("ordinary and shallow-frozen signing inputs never reuse stale nested bytes", () => { + for (const freeze of [false, true]) { + const payload = structuredClone(buildOrder({ orders: [input] }).payload) as { + type: string; + orders: (typeof input)[]; + }; + if (freeze) Object.freeze(payload); + const before = createL1ActionHash({ action: payload, nonce }); + payload.orders[0].p = "31000"; + const after = createL1ActionHash({ action: payload, nonce }); + expect(after).not.toBe(before); + expect(after).toBe(createL1ActionHash({ action: structuredClone(payload), nonce })); + } +}); + +test("a reentrant getter hashing a cached payload cannot corrupt the outer preimage", () => { + const owned = buildOrder({ orders: [input] }); + const expectedInner = createL1ActionHash({ action: owned.payload, nonce }); + const outer = { + type: "example", + get nested(): string { + expect(createL1ActionHash({ action: owned.payload, nonce })).toBe(expectedInner); + return "value"; + }, + }; + expect(createL1ActionHash({ action: outer, nonce })).toBe( + createL1ActionHash({ action: { type: "example", nested: "value" }, nonce }), + ); +}); + +test("reused action signatures still match fresh serialization on both networks and signer implementations", async () => { + const action = buildOrder({ orders: Array.from({ length: 40 }, () => input) }); + for (const wallet of [ + privateKeyToAccount(`0x${"11".repeat(32)}`), + await createFastLocalWallet(`0x${"11".repeat(32)}`), + ]) { + for (const isTestnet of [false, true]) { + const config = { + wallet, + nonceManager: () => nonce, + transport: { + isTestnet, + async request(): Promise { + throw new Error("signAction must not dispatch"); + }, + }, + }; + const first = await signAction(config, action, { vaultAddress: vault, expiresAfter: nonce + 60_000 }); + const second = await signAction(config, action, { vaultAddress: vault, expiresAfter: nonce + 60_000 }); + const fresh = await signL1Action({ + wallet, + action: structuredClone(action.payload), + nonce, + isTestnet, + vaultAddress: vault, + expiresAfter: nonce + 60_000, + }); + expect(first.signature).toEqual(second.signature); + expect(first.signature).toEqual(fresh); + } + } +}); diff --git a/packages/hyperliquid/tests/transport/_wire.test.ts b/packages/hyperliquid/tests/transport/_wire.test.ts new file mode 100644 index 00000000..5d1ea212 --- /dev/null +++ b/packages/hyperliquid/tests/transport/_wire.test.ts @@ -0,0 +1,259 @@ +/** Wire fragment reuse must preserve native serialization and error snapshots. @module */ +import { expect, test } from "bun:test"; +import { buildOrder } from "../../src/api/exchange/_methods/order.ts"; +import { executeAction, signAction, submitAction } from "../../src/actions/execution.ts"; +import { exchangeWireJSON, registerExchangeWireRequest } from "../../src/transport/_wire.ts"; +import { HttpTransport, HttpRequestError } from "../../src/transport/http/mod.ts"; +import { WebSocketTransport, WebSocketRequestError } from "../../src/transport/websocket/mod.ts"; +import { WebSocketQuota } from "../../src/transport/websocket/_quota.ts"; + +const input = { a: 0, b: true, p: "30000", s: "0.1", r: false, t: { limit: { tif: "Gtc" as const } } }; +const signature = (): { r: string; s: string; v: number } => ({ r: "0x11", s: "0x22", v: 27 }); +function request(count = 100): { + action: Readonly>; + signature: ReturnType; + nonce: number; + vaultAddress?: string; + expiresAfter?: number; +} { + const result = { + action: buildOrder({ orders: Array.from({ length: count }, () => input) }).payload, + signature: signature(), + nonce: 1_700_000_000_000, + }; + registerExchangeWireRequest(result, result.action); + return result; +} + +test("bulk wire reuse matches native JSON with fresh nonces, signatures, optional fields, and escaping", () => { + const payload = request(); + for (let i = 0; i < 5; i++) { + payload.nonce++; + payload.signature.r = `quote"\\\n${i}`; + payload.signature.v = i % 2 ? 27 : 28; + payload.vaultAddress = i % 2 ? "0x3333333333333333333333333333333333333333" : undefined; + payload.expiresAfter = i % 2 ? payload.nonce + 60_000 : undefined; + expect(exchangeWireJSON(payload)).toBe(JSON.stringify(payload)); + } + expect(exchangeWireJSON(request(1))).toBeUndefined(); +}); + +test("unregistered values and proxies are declined without extra property reads", () => { + let reads = 0; + const foreign = new Proxy(request(), { + get() { + reads++; + throw new Error("getter"); + }, + ownKeys() { + reads++; + throw new Error("keys"); + }, + }); + expect(exchangeWireJSON(foreign)).toBeUndefined(); + expect(reads).toBe(0); + expect(exchangeWireJSON({ ...request() })).toBeUndefined(); +}); + +test("mutated envelopes fall back before invoking getters, custom toJSON, or replacement signatures", () => { + let calls = 0; + const payload = request(); + exchangeWireJSON(payload); + Object.defineProperty(payload, "nonce", { + enumerable: true, + configurable: true, + get: () => { + calls++; + return 42; + }, + }); + expect(exchangeWireJSON(payload)).toBeUndefined(); + expect(calls).toBe(0); + const second = request(); + Object.defineProperty(second.signature, "toJSON", { + value: () => { + calls++; + second.nonce = 42; + return signature(); + }, + }); + expect(exchangeWireJSON(second)).toBeUndefined(); + expect(calls).toBe(0); + const third = request(); + third.signature = new Proxy(signature(), { + ownKeys() { + calls++; + throw new Error("proxy"); + }, + }); + expect(exchangeWireJSON(third)).toBeUndefined(); + expect(calls).toBe(0); + const fourth = request(); + Object.assign(fourth.signature, { v: 27n }); + // BigInt.toJSON can execute caller code even though the value is a primitive. + expect(exchangeWireJSON(fourth)).toBeUndefined(); +}); + +test("action replacement, extra fields, reordered properties, and prototype hooks decline cached JSON", () => { + const payload = request(); + exchangeWireJSON(payload); + payload.action = structuredClone(payload.action); + expect(exchangeWireJSON(payload)).toBeUndefined(); + const extra = request(); + Object.assign(extra, { extra: true }); + expect(exchangeWireJSON(extra)).toBeUndefined(); + const reordered = request(); + const action = reordered.action; + Reflect.deleteProperty(reordered, "action"); + reordered.action = action; + expect(exchangeWireJSON(reordered)).toBeUndefined(); + const hooks = request(); + for (const prototype of [Object.prototype, Array.prototype]) { + Object.defineProperty(prototype, "toJSON", { configurable: true, value: () => "hook" }); + try { + expect(exchangeWireJSON(hooks)).toBeUndefined(); + } finally { + Reflect.deleteProperty(prototype, "toJSON"); + } + } +}); + +test("HTTP sends identical bulk bodies and derives redacted errors from the sent snapshot", async () => { + const payload = request(); + const bodies: string[] = []; + const server = Bun.serve({ + port: 0, + async fetch(request) { + bodies.push(await request.text()); + return bodies.length < 3 ? Response.json({ ok: true }) : Response.json({ error: "no" }, { status: 400 }); + }, + }); + const transport = new HttpTransport({ apiUrl: server.url, timeout: null }); + try { + await transport.request("exchange", payload); + payload.nonce++; + await transport.request("exchange", payload); + payload.nonce++; + const failure = await transport.request("exchange", payload).catch((error) => error); + expect(bodies[2]).toBe(JSON.stringify(payload)); + expect(failure).toBeInstanceOf(HttpRequestError); + expect((failure as HttpRequestError).request).toEqual({ ...JSON.parse(bodies[2]), signature: "0x" }); + } finally { + await server.stop(true); + } +}); + +test("WebSocket sends identical envelopes with unique ids and preserves error snapshots", async () => { + const payload = request(); + const frames: string[] = []; + const server = Bun.serve({ + port: 0, + fetch(request, server) { + if (server.upgrade(request)) return undefined; + return new Response(null, { status: 400 }); + }, + websocket: { + message(socket, data) { + frames.push(data.toString()); + const frame = JSON.parse(data.toString()); + socket.send( + JSON.stringify({ + channel: "post", + data: { + id: frame.id, + response: + frame.id === 3 + ? { type: "error", payload: "denied" } + : { type: "action", payload: { status: "ok", response: { type: "default" } } }, + }, + }), + ); + }, + }, + }); + const transport = new WebSocketTransport({ + url: server.url.href.replace("http:", "ws:"), + keepAlive: { interval: 60_000 }, + timeout: 1000, + quota: new WebSocketQuota(), + }); + try { + await transport.request("exchange", payload); + payload.nonce++; + await transport.request("exchange", payload); + payload.nonce++; + const failure = await transport.request("exchange", payload).catch((error) => error); + expect(frames[2]).toBe(JSON.stringify({ method: "post", id: 3, request: { type: "action", payload } })); + expect(failure).toBeInstanceOf(WebSocketRequestError); + expect(JSON.stringify((failure as WebSocketRequestError).request)).not.toContain('"r":"0x11"'); + } finally { + await transport.close(); + await server.stop(true); + } +}); + +test("managed execution registers owned envelopes while preserving generic custom transport payloads", async () => { + const action = buildOrder({ orders: Array.from({ length: 100 }, () => input) }); + let nonce = 1_700_000_000_000; + const seen: string[] = []; + const config = { + wallet: { + address: "0x1111111111111111111111111111111111111111" as const, + signTypedData: async () => `0x${"11".repeat(64)}1b` as const, + }, + nonceManager: () => nonce++, + transport: { + isTestnet: true, + async request(_endpoint: string, payload: unknown): Promise { + const body = exchangeWireJSON(payload); + expect(body).toBe(JSON.stringify(payload)); + seen.push(body!); + return { status: "ok", response: { type: "default" } } as T; + }, + }, + }; + await executeAction(config, action); + await executeAction(config, action); + expect(seen[0]).not.toBe(seen[1]); +}); + +test("separate sign/submit retains immutable payload ownership, fresh signatures, and cached wire JSON", async () => { + const params = { orders: Array.from({ length: 100 }, () => structuredClone(input)) }; + const action = buildOrder(params); + const bodies: string[] = []; + let nonce = 1_700_000_000_000; + let signatures = 0; + const config = { + wallet: { + address: "0x1111111111111111111111111111111111111111" as const, + signTypedData: async () => `0x${(++signatures).toString(16).padStart(128, "0")}1b` as const, + }, + nonceManager: () => nonce++, + transport: { + isTestnet: true, + async request(_endpoint: string, payload: unknown): Promise { + const body = exchangeWireJSON(payload); + expect(body).toBe(JSON.stringify(payload)); + bodies.push(body!); + return { status: "ok", response: { type: "default" } } as T; + }, + }, + }; + const first = await signAction(config, action); + const second = await signAction(config, action); + expect(bodies).toHaveLength(0); + expect(first.action).toBe(action.payload); + expect(second.action).toBe(action.payload); + expect(first.nonce).not.toBe(second.nonce); + expect(first.signature).not.toEqual(second.signature); + expect(Object.isFrozen(first)).toBe(true); + expect(Object.isFrozen(first.signature)).toBe(true); + expect(Object.isFrozen(first.action.orders)).toBe(true); + params.orders[0].p = "1"; + expect((first.action.orders as typeof params.orders)[0].p).toBe("30000"); + await submitAction(config, first); + await submitAction(config, second); + expect(signatures).toBe(2); + expect(JSON.parse(bodies[0]).nonce).toBe(first.nonce); + expect(JSON.parse(bodies[1]).nonce).toBe(second.nonce); +});