Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/market/getOrderbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@polymarket/clob-client-v2",
"description": "TypeScript client for Polymarket's CLOB",
"version": "1.0.2",
"version": "1.0.3",
"type": "module",
"main": "dist/index.cjs",
"types": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ export class ClobClient {
* @param orderbook
* @returns
*/
public getOrderBookHash(orderbook: OrderBookSummary): string {
public async getOrderBookHash(orderbook: OrderBookSummary): Promise<string> {
Comment thread
cursor[bot] marked this conversation as resolved.
return generateOrderBookSummaryHash(orderbook);
}

Expand Down
2 changes: 1 addition & 1 deletion src/headers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const createL2Headers = async (
}
const address = await getSignerAddress(signer);

const sig = buildPolyHmacSignature(
const sig = await buildPolyHmacSignature(
creds.secret,
ts,
l2HeaderArgs.method,
Expand Down
17 changes: 0 additions & 17 deletions src/http-helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -110,15 +109,6 @@ export const del = async (endpoint: string, options?: RequestOptions): Promise<a
const errorHandling = (err: unknown) => {
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" ||
Expand All @@ -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 };
};

Expand Down
48 changes: 32 additions & 16 deletions src/signing/hmac.ts
Original file line number Diff line number Diff line change
@@ -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<string> => {
Comment thread
cursor[bot] marked this conversation as resolved.
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, "_");
};
45 changes: 34 additions & 11 deletions src/types/clob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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";
}

Expand Down Expand Up @@ -222,7 +225,7 @@ export interface BalanceAllowanceParams {

export interface BalanceAllowanceResponse {
balance: string;
allowance: string;
allowances: Record<string, string>;
}

export interface OrderScoringParams {
Expand Down Expand Up @@ -282,7 +285,15 @@ export type TokenConditionMap = Record<string, string>;
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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 7 additions & 3 deletions src/utilities.ts
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down Expand Up @@ -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<string> => {
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;
};
Expand Down
4 changes: 2 additions & 2 deletions tests/signing/hmac.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading