@nktkas/hyperliquid
Version:
Hyperliquid API SDK for all major JS runtimes, written in TypeScript.
134 lines (133 loc) • 4.9 kB
JavaScript
/**
* Schema-driven key canonicalization for Hyperliquid action objects.
* @module
*/ import { HyperliquidError } from "../_base.js";
/** Thrown when canonicalization fails due to schema/data key mismatch. */ export class CanonicalizeError extends HyperliquidError {
constructor(message){
super(message);
this.name = "CanonicalizeError";
}
}
/**
* Recursively rebuilds a value with object keys in schema-definition order.
*
* @param schema A valibot schema defining the canonical key order.
* @param value The value whose keys should be reordered.
* @return A new value with keys in schema-definition order.
*
* @throws {CanonicalizeError} If keys in data don't match the schema.
*
* @example
* ```ts
* import { canonicalize } from "@nktkas/hyperliquid/signing";
* import { CancelRequest } from "@nktkas/hyperliquid/api/exchange";
*
* const action = canonicalize(CancelRequest.entries.action, {
* type: "cancel",
* cancels: [{ a: 0, o: 12345 }],
* });
* ```
*/ export function canonicalize(schema, value) {
return walk(schema, value);
}
// ============================================================
// Recursive walker
// ============================================================
function walk(schema, value) {
const t = schema.type;
// Unwrap optional / nullable / nullish
if (t === "optional" || t === "nullable" || t === "nullish") {
return value === null || value === undefined ? value : walk(schema.wrapped, value);
}
// Object → reorder keys by schema.entries
if (t === "object" && isRecord(value)) {
return reorderObject(schema.entries, value);
}
// Array → canonicalize each item
if (t === "array" && Array.isArray(value)) {
return value.map((item)=>walk(schema.item, item));
}
// Tuple → canonicalize each item by index
if (t === "tuple" && Array.isArray(value)) {
return value.map((item, i)=>walk(schema.items[i], item));
}
// Variant → match option by discriminator + structural fallback
if (t === "variant" && isRecord(value)) {
const option = matchVariantOption(schema.key, schema.options, value);
if (option) return walk(option, value);
throw new CanonicalizeError(`No variant option matches data (discriminator "${schema.key}" = ${JSON.stringify(value[schema.key])})`);
}
// Union → match option structurally
if (t === "union" && isRecord(value)) {
const option = matchByStructure(schema.options, value);
if (option) return walk(option, value);
return value;
}
// Primitives, literals, picklists, enums → return as-is
return value;
}
// ============================================================
// Helpers
// ============================================================
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function reorderObject(entries, value) {
// --- Reject extra keys not in schema ---------------------
for (const key of Object.keys(value)){
if (!(key in entries)) {
throw new CanonicalizeError(`Key "${key}" exists in data but not in schema`);
}
}
// --- Reject missing required keys ------------------------
for (const key of Object.keys(entries)){
if (!(key in value)) {
const t = entries[key].type;
if (t !== "optional" && t !== "nullable" && t !== "nullish") {
throw new CanonicalizeError(`Required key "${key}" exists in schema but not in data`);
}
}
}
// --- Build reordered result ------------------------------
const result = {};
for (const key of Object.keys(entries)){
if (key in value) {
result[key] = walk(entries[key], value[key]);
}
}
return result;
}
function matchVariantOption(discriminatorKey, options, value) {
const discriminatorValue = value[discriminatorKey];
const matching = [];
for (const option of options){
if (option.type === "object" && option.entries && discriminatorKey in option.entries) {
const keySchema = option.entries[discriminatorKey];
if (keySchema.type === "literal" && keySchema.literal === discriminatorValue) {
matching.push(option);
}
}
}
if (matching.length === 1) return matching[0];
if (matching.length > 1) return matchByStructure(matching, value);
return undefined;
}
function matchByStructure(options, value) {
const dataKeys = new Set(Object.keys(value));
for (const option of options){
if (option.type === "object" && option.entries) {
if ([
...dataKeys
].every((k)=>k in option.entries)) {
const allRequired = Object.keys(option.entries).every((k)=>{
if (dataKeys.has(k)) return true;
const t = option.entries[k].type;
return t === "optional" || t === "nullable" || t === "nullish";
});
if (allRequired) return option;
}
}
}
return undefined;
}
//# sourceMappingURL=_canonicalize.js.map