Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ jobs:
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm publish --no-git-checks
- run: |
if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then
pnpm publish --no-git-checks --tag canary
else
pnpm publish --no-git-checks
fi
250 changes: 250 additions & 0 deletions examples/account/approveDepositWalletAllowances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import { resolve } from "node:path";
import { config as dotenvConfig } from "dotenv";
import { createPublicClient, encodeFunctionData, http, maxUint256, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { polygon, polygonAmoy } from "viem/chains";

import { Chain } from "../../src";
import { getContractConfig } from "../../src/config";

dotenvConfig({ path: resolve(__dirname, "../../.env") });

const WALLET_ABI = parseAbi(["function nonce() view returns (uint256)"]);
const ERC20_ABI = parseAbi([
"function allowance(address,address) view returns (uint256)",
"function approve(address,uint256) returns (bool)",
]);
const CTF_ABI = parseAbi([
"function isApprovedForAll(address,address) view returns (bool)",
"function setApprovalForAll(address,bool)",
]);

const BATCH_EIP712_TYPES = {
Batch: [
{ name: "wallet", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "calls", type: "Call[]" },
],
Call: [
{ name: "target", type: "address" },
{ name: "value", type: "uint256" },
{ name: "data", type: "bytes" },
],
} as const;

const DEADLINE_OFFSET = 240n; // 4 min; relayer max is 300s

async function gammaLogin(
account: ReturnType<typeof privateKeyToAccount>,
chainId: number,
gammaUrl: string,
): Promise<string> {
const nonceRes = await fetch(`${gammaUrl}/nonce`);
if (!nonceRes.ok) throw new Error(`/nonce ${nonceRes.status}`);
const { nonce } = (await nonceRes.json()) as { nonce: string };
const nonceCookies = parseCookies(nonceRes.headers);

const issuedAt = new Date().toISOString();
const expiration = new Date(Date.now() + 7 * 24 * 3_600_000).toISOString();
const domain = "polymarket.com";

const siweMessage = [
`${domain} wants you to sign in with your Ethereum account:`,
account.address,
"",
"Welcome to Polymarket! Sign to connect.",
"",
`URI: https://${domain}`,
"Version: 1",
`Chain ID: ${chainId}`,
`Nonce: ${nonce}`,
`Issued At: ${issuedAt}`,
`Expiration Time: ${expiration}`,
].join("\n");

const signature = await account.signMessage({ message: siweMessage });

const jsonPayload = JSON.stringify({
domain,
address: account.address,
statement: "Welcome to Polymarket! Sign to connect.",
uri: `https://${domain}`,
version: "1",
chainId,
nonce,
issuedAt,
expirationTime: expiration,
});
const authToken = Buffer.from(`${jsonPayload}:::${signature}`).toString("base64");

const loginRes = await fetch(`${gammaUrl}/login`, {
headers: { Authorization: `Bearer ${authToken}`, Cookie: nonceCookies },
});
if (!loginRes.ok) throw new Error(`/login ${loginRes.status}: ${await loginRes.text()}`);

const loginCookies = parseCookies(loginRes.headers);
console.log("Gamma login successful");
return mergeCookies(nonceCookies, loginCookies);
}

async function submitBatch(
account: ReturnType<typeof privateKeyToAccount>,
chainId: number,
wallet: `0x${string}`,
factory: `0x${string}`,
nonce: bigint,
calls: Array<{ target: `0x${string}`; value: bigint; data: `0x${string}` }>,
cookies: string,
relayerUrl: string,
): Promise<string> {
const deadline = BigInt(Math.floor(Date.now() / 1000)) + DEADLINE_OFFSET;

const sig = await account.signTypedData({
domain: { name: "DepositWallet", version: "1", chainId, verifyingContract: wallet },
types: BATCH_EIP712_TYPES,
primaryType: "Batch",
message: { wallet, nonce, deadline, calls },
});

const resp = await fetch(`${relayerUrl}/submit`, {
method: "POST",
headers: { "Content-Type": "application/json", Cookie: cookies },
body: JSON.stringify({
type: "WALLET",
from: account.address,
to: factory,
nonce: nonce.toString(),
signature: sig,
depositWalletParams: {
depositWallet: wallet,
deadline: deadline.toString(),
calls: calls.map((c) => ({ target: c.target, value: c.value.toString(), data: c.data })),
},
}),
});
if (!resp.ok) throw new Error(`Submit failed ${resp.status}: ${await resp.text()}`);

const result = (await resp.json()) as { transactionID: string; state: string };
console.log(` submitted txnID=${result.transactionID} state=${result.state}`);
return result.transactionID;
}

async function pollConfirmed(txnId: string, relayerUrl: string): Promise<void> {
while (true) {
const resp = await fetch(`${relayerUrl}/transaction?id=${txnId}`);
const data = (await resp.json()) as Array<{ state: string; transactionHash?: string }>;
const state = data[0]?.state ?? "UNKNOWN";
console.log(` state=${state}`);
if (state === "STATE_CONFIRMED") {
console.log(` txHash=${data[0]?.transactionHash}`);
return;
}
if (state === "STATE_FAILED") throw new Error(`Transaction ${txnId} failed`);
await new Promise((r) => setTimeout(r, 3000));
}
}

async function main() {
const isMainnet = true;

const pk = process.env.PK as `0x${string}`;
const rpcUrl = process.env.RPC_URL as string;
const wallet = process.env.DEPOSIT_WALLET as `0x${string}`;
const factory = process.env.DEPOSIT_WALLET_FACTORY as `0x${string}`;
const gammaUrl = process.env.GAMMA_API_URL as string;
const relayerUrl = process.env.RELAYER_API_URL as string;

if (!pk || !rpcUrl || !wallet || !factory || !gammaUrl || !relayerUrl) {
throw new Error(
"Missing required env: PK, RPC_URL, DEPOSIT_WALLET, DEPOSIT_WALLET_FACTORY, GAMMA_API_URL, RELAYER_API_URL",
);
}

const chainId = isMainnet ? Chain.POLYGON : Chain.AMOY;
const viemChain = isMainnet ? polygon : polygonAmoy;
const contracts = getContractConfig(chainId);

const account = privateKeyToAccount(pk);
const publicClient = createPublicClient({ chain: viemChain, transport: http(rpcUrl) });

console.log(`Signer: ${account.address}`);
console.log(`Deposit wallet: ${wallet}`);
console.log(`Chain ID: ${chainId}`);
console.log(`USDC: ${contracts.collateral}`);
console.log(`CTF: ${contracts.conditionalTokens}`);
console.log(`Exchange V2: ${contracts.exchangeV2}`);

const usdc = contracts.collateral as `0x${string}`;
const ctf = contracts.conditionalTokens as `0x${string}`;
const exchange = contracts.exchangeV2 as `0x${string}`;

const [usdcAllowanceCtf, usdcAllowanceExchange, ctfApprovedExchange] = await Promise.all([
publicClient.readContract({ address: usdc, abi: ERC20_ABI, functionName: "allowance", args: [wallet, ctf] }),
publicClient.readContract({ address: usdc, abi: ERC20_ABI, functionName: "allowance", args: [wallet, exchange] }),
publicClient.readContract({ address: ctf, abi: CTF_ABI, functionName: "isApprovedForAll", args: [wallet, exchange] }),
]);

console.log(`\nCurrent state:`);
console.log(` USDC → CTF: ${usdcAllowanceCtf}`);
console.log(` USDC → Exchange V2: ${usdcAllowanceExchange}`);
console.log(` CTF → Exchange V2: ${ctfApprovedExchange}`);

const needsUsdcCtf = usdcAllowanceCtf === 0n;
const needsUsdcExchange = usdcAllowanceExchange === 0n;
const needsCtfExchange = !ctfApprovedExchange;

if (!needsUsdcCtf && !needsUsdcExchange && !needsCtfExchange) {
console.log("\nAll approvals already set — nothing to do");
return;
}

console.log("\nLogging into Gamma...");
const cookies = await gammaLogin(account, chainId, gammaUrl);

if (needsUsdcCtf) {
console.log("\nApproving USDC → CTF...");
const nonce = await publicClient.readContract({ address: wallet, abi: WALLET_ABI, functionName: "nonce" });
const data = encodeFunctionData({ abi: ERC20_ABI, functionName: "approve", args: [ctf, maxUint256] });
const txnId = await submitBatch(account, chainId, wallet, factory, nonce, [{ target: usdc, value: 0n, data }], cookies, relayerUrl);
await pollConfirmed(txnId, relayerUrl);
}

if (needsUsdcExchange) {
console.log("\nApproving USDC → Exchange V2...");
const nonce = await publicClient.readContract({ address: wallet, abi: WALLET_ABI, functionName: "nonce" });
const data = encodeFunctionData({ abi: ERC20_ABI, functionName: "approve", args: [exchange, maxUint256] });
const txnId = await submitBatch(account, chainId, wallet, factory, nonce, [{ target: usdc, value: 0n, data }], cookies, relayerUrl);
await pollConfirmed(txnId, relayerUrl);
}

if (needsCtfExchange) {
console.log("\nSetting CTF approval for Exchange V2...");
const nonce = await publicClient.readContract({ address: wallet, abi: WALLET_ABI, functionName: "nonce" });
const data = encodeFunctionData({ abi: CTF_ABI, functionName: "setApprovalForAll", args: [exchange, true] });
const txnId = await submitBatch(account, chainId, wallet, factory, nonce, [{ target: ctf, value: 0n, data }], cookies, relayerUrl);
await pollConfirmed(txnId, relayerUrl);
}

console.log("\nAll approvals done");
}

function parseCookies(headers: Headers): string {
const raw: string[] =
typeof (headers as any).getSetCookie === "function"
? (headers as any).getSetCookie()
: (headers.get("set-cookie") ?? "").split(/,(?=[^ ])/).filter(Boolean);
return raw.map((c) => c.split(";")[0]).join("; ");
}

function mergeCookies(a: string, b: string): string {
const map = new Map<string, string>();
for (const part of [...a.split("; "), ...b.split("; ")]) {
const eq = part.indexOf("=");
if (eq === -1) continue;
map.set(part.slice(0, eq).trim(), part.trim());
}
return [...map.values()].join("; ");
}

main();
11 changes: 11 additions & 0 deletions examples/keys/signatureTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ async function main() {
signatureType: SignatureTypeV2.POLY_GNOSIS_SAFE,
funderAddress: gnosisSafeAddress,
});

// Client used with a Polymarket Deposit Wallet: Signature type 3 (POLY_1271)
const depositWalletAddress = "0x...";
const depositWalletClient = new ClobClient({
host,
chain: chainId,
signer: walletClient,
creds,
signatureType: SignatureTypeV2.POLY_1271,
funderAddress: depositWalletAddress,
});
}

main();
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-canary.0",
"type": "module",
"main": "dist/index.cjs",
"types": "dist/index.d.ts",
Expand Down
13 changes: 12 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { LocalAccount } from "viem";

import {
BUILDER_FEES_BPS,
bytes32Zero,
Expand Down Expand Up @@ -84,7 +86,7 @@ import {
calculateSellMarketPrice,
} from "./order-builder/helpers/index.js";
import { OrderBuilder } from "./order-builder/index.js";
import type { SignatureTypeV2 } from "./order-utils/model/signatureTypeV2.js";
import { SignatureTypeV2 } from "./order-utils/model/signatureTypeV2.js";
import type { ClobSigner } from "./signing/signer.js";
import type {
ApiKeyCreds,
Expand Down Expand Up @@ -179,6 +181,7 @@ export interface ClobClientOptions {
getSigner?: () => Promise<ClobSigner> | ClobSigner;
retryOnError?: boolean;
throwOnError?: boolean;
sessionSigner?: LocalAccount;
}

export class ClobClient {
Expand Down Expand Up @@ -211,6 +214,10 @@ export class ClobClient {

readonly builderConfig?: BuilderConfig;

readonly signatureType: SignatureTypeV2;

readonly funderAddress?: string;

private cachedVersion?: number;

readonly retryOnError?: boolean;
Expand All @@ -229,6 +236,7 @@ export class ClobClient {
getSigner,
retryOnError,
throwOnError,
sessionSigner,
}: ClobClientOptions) {
this.host = host.endsWith("/") ? host.slice(0, -1) : host;
this.chainId = chain;
Expand All @@ -245,7 +253,10 @@ export class ClobClient {
signatureType,
funderAddress,
getSigner,
sessionSigner,
);
this.signatureType = signatureType ?? SignatureTypeV2.EOA;
this.funderAddress = funderAddress;
this.tickSizes = {};
this.negRisk = {};
this.feeRates = {};
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export const bytes32Zero = "0x00000000000000000000000000000000000000000000000000
export const ORDER_VERSION_MISMATCH_ERROR = "order_version_mismatch";

export const BUILDER_FEES_BPS = 10000;

export const SESSION_SIGNER_MAGIC =
"6492649264926492649264926492649264926492649264926492649264926492";
7 changes: 4 additions & 3 deletions src/headers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const createL1Headers = async (
chainId: Chain,
nonce?: number,
timestamp?: number,
address?: string,
): Promise<L1PolyHeader> => {
let ts = Math.floor(Date.now() / 1000);
if (timestamp !== undefined) {
Expand All @@ -27,11 +28,11 @@ export const createL1Headers = async (
n = nonce;
}

const sig = await buildClobEip712Signature(signer, chainId, ts, n);
const address = await getSignerAddress(signer);
const sig = await buildClobEip712Signature(signer, chainId, ts, n, address);
const resolvedAddress = address ?? (await getSignerAddress(signer));

const headers = {
POLY_ADDRESS: address,
POLY_ADDRESS: resolvedAddress,
POLY_SIGNATURE: sig,
POLY_TIMESTAMP: `${ts}`,
POLY_NONCE: `${n}`,
Expand Down
Loading
Loading