@nktkas/hyperliquid
Version:
Hyperliquid API SDK for all major JS runtimes, written in TypeScript.
223 lines (222 loc) • 7.96 kB
JavaScript
/**
* Abstract wallet interfaces and signing utilities for [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data.
* @module
*/ import { HyperliquidError } from "../_base.js";
// ============================================================
// Error
// ============================================================
/** Thrown when an error occurs in AbstractWallet operations (e.g., signing, getting address). */ export class AbstractWalletError extends HyperliquidError {
constructor(message, options){
super(message, options);
this.name = "AbstractWalletError";
}
}
/** Parse a 65-byte hex signature into `{r, s, v}`. Normalizes raw recovery 0/1 to 27/28. */ function parseSignature(hex) {
if (hex.length !== 132) {
throw new AbstractWalletError(`Expected 65-byte signature (132 hex chars), got ${hex.length}`);
}
const r = `0x${hex.slice(2, 66)}`;
const s = `0x${hex.slice(66, 130)}`;
let v = parseInt(hex.slice(130, 132), 16);
if (v === 0 || v === 1) v += 27;
if (v !== 27 && v !== 28) {
throw new AbstractWalletError(`Invalid signature recovery value: ${v}, expected 0/1 or 27/28`);
}
return {
r,
s,
v
};
}
function isEthersV6Signer(wallet) {
return "signTypedData" in wallet && typeof wallet.signTypedData === "function" && wallet.signTypedData.length === 3 && "getAddress" in wallet && typeof wallet.getAddress === "function";
}
function adaptEthersV6(wallet) {
return {
kind: "ethers-v6",
async signTypedData (args) {
const hex = await wallet.signTypedData(args.domain, args.types, args.message);
return parseSignature(hex);
},
async getAddress () {
const address = await wallet.getAddress();
return address.toLowerCase();
},
async getChainId () {
if (!wallet.provider) return "0x1";
const network = await wallet.provider.getNetwork();
return `0x${network.chainId.toString(16)}`;
}
};
}
function isEthersV5Signer(wallet) {
return "_signTypedData" in wallet && typeof wallet._signTypedData === "function" && wallet._signTypedData.length === 3 && "getAddress" in wallet && typeof wallet.getAddress === "function";
}
function adaptEthersV5(wallet) {
return {
kind: "ethers-v5",
async signTypedData (args) {
const hex = await wallet._signTypedData(args.domain, args.types, args.message);
return parseSignature(hex);
},
async getAddress () {
const address = await wallet.getAddress();
return address.toLowerCase();
},
async getChainId () {
if (!wallet.provider) return "0x1";
const network = await wallet.provider.getNetwork();
return `0x${network.chainId.toString(16)}`;
}
};
}
// ============================================================
// Viem JSON-RPC Account
// ============================================================
/** EIP-712 domain type definition; viem wallet adapters require it in `types`. */ const EIP712_DOMAIN_TYPE = [
{
name: "name",
type: "string"
},
{
name: "version",
type: "string"
},
{
name: "chainId",
type: "uint256"
},
{
name: "verifyingContract",
type: "address"
}
];
function isViemJsonRpc(wallet) {
return "signTypedData" in wallet && typeof wallet.signTypedData === "function" && (wallet.signTypedData.length === 1 || wallet.signTypedData.length === 2) && "getAddresses" in wallet && typeof wallet.getAddresses === "function" && "getChainId" in wallet && typeof wallet.getChainId === "function";
}
function adaptViemJsonRpc(wallet) {
return {
kind: "viem-jsonrpc",
async signTypedData (args) {
const hex = await wallet.signTypedData({
domain: args.domain,
types: {
EIP712Domain: EIP712_DOMAIN_TYPE,
...args.types
},
primaryType: args.primaryType,
message: args.message
});
return parseSignature(hex);
},
async getAddress () {
const addresses = await wallet.getAddresses();
if (!addresses.length) throw new AbstractWalletError("Wallet returned no addresses");
return addresses[0].toLowerCase();
},
async getChainId () {
const id = await wallet.getChainId();
return `0x${id.toString(16)}`;
}
};
}
function isViemLocal(wallet) {
return "signTypedData" in wallet && typeof wallet.signTypedData === "function" && (wallet.signTypedData.length === 1 || wallet.signTypedData.length === 2) && "address" in wallet && typeof wallet.address === "string";
}
function adaptViemLocal(wallet) {
return {
kind: "viem-local",
async signTypedData (args) {
const hex = await wallet.signTypedData({
domain: args.domain,
types: {
EIP712Domain: EIP712_DOMAIN_TYPE,
...args.types
},
primaryType: args.primaryType,
message: args.message
});
return parseSignature(hex);
},
getAddress () {
return Promise.resolve(wallet.address.toLowerCase());
},
getChainId () {
// Local accounts have no notion of chain; default to "0x1".
return Promise.resolve("0x1");
}
};
}
/** Adapt a wallet of any supported kind to the uniform {@link Signer} interface. */ function adapt(wallet) {
if (isViemJsonRpc(wallet)) return adaptViemJsonRpc(wallet);
if (isViemLocal(wallet)) return adaptViemLocal(wallet);
if (isEthersV6Signer(wallet)) return adaptEthersV6(wallet);
if (isEthersV5Signer(wallet)) return adaptEthersV5(wallet);
throw new AbstractWalletError("Failed to adapt wallet: unknown wallet type");
}
// ============================================================
// Public API
// ============================================================
/**
* Signs [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data using the provided wallet.
*
* @param args The wallet, domain, types, primary type, and message to sign.
* @return The ECDSA signature components.
*
* @throws {AbstractWalletError} If the wallet type is unknown or signing fails.
*/ export async function signTypedData(args) {
try {
// Filter message to only contain fields defined in types (required by some wallets)
const typeFields = args.types[args.primaryType];
const message = typeFields ? Object.fromEntries(Object.entries(args.message).filter(([k])=>typeFields.some((f)=>f.name === k))) : args.message;
return await adapt(args.wallet).signTypedData({
domain: args.domain,
types: args.types,
primaryType: args.primaryType,
message
});
} catch (error) {
if (error instanceof AbstractWalletError) throw error;
throw new AbstractWalletError(`Failed to sign the typed data using the wallet`, {
cause: error
});
}
}
/**
* Gets the lowercase wallet address from various wallet types.
*
* @param wallet The wallet to query.
* @return The lowercase wallet address as a hex string.
*
* @throws {AbstractWalletError} If getting the address fails or the wallet type is unknown.
*/ export async function getWalletAddress(wallet) {
try {
return await adapt(wallet).getAddress();
} catch (error) {
if (error instanceof AbstractWalletError) throw error;
throw new AbstractWalletError("Failed to get an address from the wallet", {
cause: error
});
}
}
/**
* Gets the chain ID of the wallet.
*
* For wallets that have no notion of chain (e.g., a viem local account, or an ethers signer without a provider),
* defaults to `"0x1"`.
*
* @param wallet The wallet to query.
* @return The chain ID as a hex string.
*
* @throws {AbstractWalletError} If getting the chain ID fails or the wallet type is unknown.
*/ export async function getWalletChainId(wallet) {
try {
return await adapt(wallet).getChainId();
} catch (error) {
if (error instanceof AbstractWalletError) throw error;
throw new AbstractWalletError("Failed to get the chain ID from the wallet", {
cause: error
});
}
}
//# sourceMappingURL=_abstractWallet.js.map