UNPKG

@nktkas/hyperliquid

Version:

Hyperliquid API SDK for all major JS runtimes, written in TypeScript.

223 lines (222 loc) 7.96 kB
/** * 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