diff --git a/examples/market/getOrderbook.ts b/examples/market/getOrderbook.ts index 73969fe..f482598 100644 --- a/examples/market/getOrderbook.ts +++ b/examples/market/getOrderbook.ts @@ -14,7 +14,7 @@ async function main() { const orderbook = await clobClient.getOrderBook(YES); console.log("orderbook", orderbook); - const hash = clobClient.getOrderBookHash(orderbook); + const hash = await clobClient.getOrderBookHash(orderbook); console.log("orderbook hash", hash); } diff --git a/package.json b/package.json index ebbff88..bfd418b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@polymarket/clob-client-v2", "description": "TypeScript client for Polymarket's CLOB", - "version": "1.0.3", + "version": "1.0.4", "type": "module", "main": "dist/index.cjs", "types": "dist/index.d.ts", @@ -25,6 +25,9 @@ "blockchain", "ethereum" ], + "engines": { + "node": ">=20.10" + }, "packageManager": "pnpm@10.30.3", "license": "MIT", "repository": { diff --git a/src/client.ts b/src/client.ts index f7cae90..d3ce4a5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -427,7 +427,7 @@ export class ClobClient { * @param orderbook * @returns */ - public getOrderBookHash(orderbook: OrderBookSummary): string { + public async getOrderBookHash(orderbook: OrderBookSummary): Promise { return generateOrderBookSummaryHash(orderbook); } diff --git a/src/headers/index.ts b/src/headers/index.ts index c1cad11..3b93d2a 100644 --- a/src/headers/index.ts +++ b/src/headers/index.ts @@ -52,7 +52,7 @@ export const createL2Headers = async ( } const address = await getSignerAddress(signer); - const sig = buildPolyHmacSignature( + const sig = await buildPolyHmacSignature( creds.secret, ts, l2HeaderArgs.method, diff --git a/src/http-helpers/index.ts b/src/http-helpers/index.ts index 7e74435..7f08959 100644 --- a/src/http-helpers/index.ts +++ b/src/http-helpers/index.ts @@ -64,7 +64,6 @@ export const post = async ( return resp.data; } catch (err: unknown) { if (retryOnError && isTransientAxiosError(err)) { - console.log("[CLOB Client-v2] transient error, retrying once after 30 ms"); await sleep(30); try { const resp = await request( @@ -110,15 +109,6 @@ export const del = async (endpoint: string, options?: RequestOptions): Promise { if (axios.isAxiosError(err)) { if (err.response) { - console.error( - "[CLOB Client] request error", - JSON.stringify({ - status: err.response?.status, - statusText: err.response?.statusText, - data: err.response?.data, - config: err.response?.config, - }), - ); if (err.response?.data) { if ( typeof err.response?.data === "string" || @@ -136,17 +126,10 @@ const errorHandling = (err: unknown) => { } if (err.message) { - console.error( - "[CLOB Client] request error", - JSON.stringify({ - error: err.message, - }), - ); return { error: err.message }; } } - console.error("[CLOB Client] request error", err); return { error: err }; }; diff --git a/src/signing/hmac.ts b/src/signing/hmac.ts index bab280d..30c579b 100644 --- a/src/signing/hmac.ts +++ b/src/signing/hmac.ts @@ -1,35 +1,51 @@ -import crypto from "node:crypto"; - -function replaceAll(s: string, search: string, replace: string) { - return s.split(search).join(replace); -} - /** * Builds the canonical Polymarket CLOB HMAC signature - * @param signer - * @param key * @param secret - * @param passphrase + * @param timestamp + * @param method + * @param requestPath + * @param body * @returns string */ -export const buildPolyHmacSignature = ( +export const buildPolyHmacSignature = async ( secret: string, timestamp: number, method: string, requestPath: string, body?: string, -): string => { +): Promise => { let message = timestamp + method + requestPath; if (body !== undefined) { message += body; } - const base64Secret = Buffer.from(secret, "base64"); - const hmac = crypto.createHmac("sha256", base64Secret); - const sig = hmac.update(message).digest("base64"); + const binarySecret = atob(secret); + const keyBytes = new Uint8Array(binarySecret.length); + for (let i = 0; i < binarySecret.length; i++) { + keyBytes[i] = binarySecret.charCodeAt(i); + } + + const key = await globalThis.crypto.subtle.importKey( + "raw", + keyBytes, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + + const sigBuffer = await globalThis.crypto.subtle.sign( + "HMAC", + key, + new TextEncoder().encode(message), + ); + + const sigBytes = new Uint8Array(sigBuffer); + let binary = ""; + for (let i = 0; i < sigBytes.length; i++) { + binary += String.fromCharCode(sigBytes[i]); + } // NOTE: Must be url safe base64 encoding, but keep base64 "=" suffix // Convert '+' to '-' // Convert '/' to '_' - const sigUrlSafe = replaceAll(replaceAll(sig, "+", "-"), "/", "_"); - return sigUrlSafe; + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_"); }; diff --git a/src/types/clob.ts b/src/types/clob.ts index 5ff9a05..b2f3a5c 100644 --- a/src/types/clob.ts +++ b/src/types/clob.ts @@ -58,7 +58,8 @@ export interface OrderResponse { success: boolean; errorMsg: string; orderID: string; - transactionsHashes: string[]; + transactionsHashes?: string[]; + tradeIDs?: string[]; status: string; takingAmount: string; makingAmount: string; @@ -97,14 +98,14 @@ export interface MakerOrder { fee_rate_bps: string; asset_id: string; outcome: string; - side: Side; + side?: Side; + builder_fee?: string; + builder_code?: string; } export interface Trade { id: string; - taker_order_id: string; - market: string; asset_id: string; side: Side; @@ -113,13 +114,15 @@ export interface Trade { price: string; status: string; match_time: string; + match_time_nano?: string; last_update: string; outcome: string; bucket_index: number; owner: string; maker_address: string; maker_orders: MakerOrder[]; - transaction_hash: string; + transaction_hash?: string; + err_msg?: string | null; trader_side: "TAKER" | "MAKER"; } @@ -222,7 +225,7 @@ export interface BalanceAllowanceParams { export interface BalanceAllowanceResponse { balance: string; - allowance: string; + allowances: Record; } export interface OrderScoringParams { @@ -282,7 +285,15 @@ export type TokenConditionMap = Record; export interface FeeDetails { r?: number; // fee rate e?: number; // fee exponent - to: boolean; // taker only + to?: boolean; // taker only (omitted when false) +} + +export interface ClobRewards { + mi?: number; // min size + ma?: number; // max spread + e?: boolean; // enabled + smoa?: boolean; // skip min order age + moas?: number; // min order age seconds } export interface ClobToken { @@ -292,12 +303,22 @@ export interface ClobToken { export interface MarketDetails { c: string; // condition ID - t: [ClobToken | null, ClobToken | null]; // YES and NO tokens + t: [ClobToken, ClobToken]; // YES and NO tokens mts: number; // min tick size - nr: boolean; // neg risk + nr?: boolean; // neg risk (omitted when false) fd?: FeeDetails; // platform fee details - mbf?: number; // v1 maker base fee - tbf?: number; // v1 taker base fee + mbf?: number; // maker base fee + tbf?: number; // taker base fee + r: ClobRewards | null; // rewards config (always present, null if unset) + ao?: boolean; // accepting orders + mos?: number; // min order size + sd?: number; // seconds delay + gst?: string; // game start time (ISO 8601) + cbos?: boolean; // clear book on start + aot?: string; // accepting orders timestamp (ISO 8601) + rfqe?: boolean; // RFQ enabled + itode?: boolean; // taker order delay enabled + ibce?: boolean; // blockaid check enabled } export interface PaginationPayload { @@ -402,6 +423,8 @@ export interface BuilderTrade { bucketIndex: number; fee: string; feeUsdc: string; + builderFee: string; + builderCode: string; err_msg?: string | null; createdAt: string | null; updatedAt: string | null; diff --git a/src/utilities.ts b/src/utilities.ts index a73ee6d..94ef07c 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -1,4 +1,3 @@ -import { createHash } from "node:crypto"; import type { OrderBookSummary, TickSize } from "./types/index.js"; export const roundNormal = (num: number, decimals: number): number => { @@ -40,9 +39,14 @@ export const decimalPlaces = (num: number): number => { * @param orderbook * @returns */ -export const generateOrderBookSummaryHash = (orderbook: OrderBookSummary): string => { +export const generateOrderBookSummaryHash = async ( + orderbook: OrderBookSummary, +): Promise => { orderbook.hash = ""; - const hash = createHash("sha1").update(JSON.stringify(orderbook)).digest("hex"); + const data = new TextEncoder().encode(JSON.stringify(orderbook)); + const hashBuffer = await globalThis.crypto.subtle.digest("SHA-1", data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hash = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); orderbook.hash = hash; return hash; }; diff --git a/tests/signing/hmac.test.ts b/tests/signing/hmac.test.ts index b4f8adc..84867e6 100644 --- a/tests/signing/hmac.test.ts +++ b/tests/signing/hmac.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest"; import { buildPolyHmacSignature } from "../../src/signing/hmac"; describe("hmac", () => { - it("buildPolyHmacSignature", () => { - const signature = buildPolyHmacSignature( + it("buildPolyHmacSignature", async () => { + const signature = await buildPolyHmacSignature( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", 1000000, "test-sign",