Skip to content

Commit f5c8bcc

Browse files
committed
ft: add clob 1271 order signing changes
1 parent 0a735c6 commit f5c8bcc

4 files changed

Lines changed: 316 additions & 0 deletions

File tree

src/exchange.order-v2.builder.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import type { JsonRpcSigner } from '@ethersproject/providers';
2+
import type { Wallet } from '@ethersproject/wallet';
3+
import {
4+
EIP712_DOMAIN,
5+
PROTOCOL_NAME,
6+
} from './exchange.order.const.ts';
7+
import {
8+
PROTOCOL_VERSION_V2,
9+
ORDER_V2_STRUCTURE,
10+
} from './exchange.order-v2.const.ts';
11+
import type { EIP712TypedData } from './model/eip712.model.ts';
12+
import { hashTypedData } from 'viem';
13+
import type {
14+
OrderV2,
15+
OrderDataV2,
16+
OrderHashV2,
17+
OrderSignatureV2,
18+
SignedOrderV2,
19+
} from './model/order-v2.model.ts';
20+
import { SignatureType } from './model/signature-types.model.ts';
21+
import { generateOrderSalt } from './utils.ts';
22+
23+
const BYTES32_ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000';
24+
25+
export class ExchangeOrderBuilderV2 {
26+
constructor(
27+
private readonly contractAddress: string,
28+
private readonly chainId: number,
29+
private readonly signer: Wallet | JsonRpcSigner,
30+
private readonly generateSalt = generateOrderSalt,
31+
) {}
32+
33+
/**
34+
* Build an order object including the signature.
35+
* @param orderData
36+
* @returns a SignedOrderV2 object (order + signature)
37+
*/
38+
async buildSignedOrder(orderData: OrderDataV2): Promise<SignedOrderV2> {
39+
const order = await this.buildOrder(orderData);
40+
const orderTypedData = this.buildOrderTypedData(order);
41+
const orderSignature = await this.buildOrderSignature(orderTypedData);
42+
43+
return {
44+
...order,
45+
signature: orderSignature,
46+
} as SignedOrderV2;
47+
}
48+
49+
/**
50+
* Creates an OrderV2 object from order data.
51+
* @param orderData
52+
* @returns an OrderV2 object (not signed)
53+
*/
54+
async buildOrder({
55+
maker,
56+
tokenId,
57+
makerAmount,
58+
takerAmount,
59+
side,
60+
signer,
61+
signatureType,
62+
timestamp,
63+
metadata,
64+
builder,
65+
expiration,
66+
}: OrderDataV2): Promise<OrderV2> {
67+
if (typeof signer == 'undefined' || !signer) {
68+
signer = maker;
69+
}
70+
71+
const resolvedSignatureType = signatureType ?? SignatureType.EOA;
72+
73+
// For POLY_DEPOSIT_WALLET, the order's signer field is the wallet
74+
// contract address, while the actual ECDSA signing is done by the EOA owner
75+
if (resolvedSignatureType !== SignatureType.POLY_DEPOSIT_WALLET) {
76+
const signerAddress = await this.signer.getAddress();
77+
if (signer !== signerAddress) {
78+
throw new Error('signer does not match');
79+
}
80+
}
81+
82+
return {
83+
salt: this.generateSalt(),
84+
maker,
85+
signer,
86+
tokenId,
87+
makerAmount,
88+
takerAmount,
89+
side,
90+
signatureType: resolvedSignatureType,
91+
timestamp: timestamp ?? Date.now().toString(),
92+
metadata: metadata ?? BYTES32_ZERO,
93+
builder: builder ?? BYTES32_ZERO,
94+
expiration: expiration ?? '0',
95+
};
96+
}
97+
98+
/**
99+
* Parses an OrderV2 object to EIP712 typed data
100+
* @param order
101+
* @returns a EIP712TypedData object
102+
*/
103+
buildOrderTypedData(order: OrderV2): EIP712TypedData {
104+
return {
105+
primaryType: 'Order',
106+
types: {
107+
EIP712Domain: EIP712_DOMAIN,
108+
Order: ORDER_V2_STRUCTURE,
109+
},
110+
domain: {
111+
name: PROTOCOL_NAME,
112+
version: PROTOCOL_VERSION_V2,
113+
chainId: this.chainId,
114+
verifyingContract: this.contractAddress,
115+
},
116+
message: {
117+
salt: order.salt,
118+
maker: order.maker,
119+
signer: order.signer,
120+
tokenId: order.tokenId,
121+
makerAmount: order.makerAmount,
122+
takerAmount: order.takerAmount,
123+
side: order.side,
124+
signatureType: order.signatureType,
125+
timestamp: order.timestamp,
126+
metadata: order.metadata,
127+
builder: order.builder,
128+
},
129+
};
130+
}
131+
132+
/**
133+
* Generates order's signature from a EIP712TypedData object + the signer address
134+
* @param typedData
135+
* @returns a OrderSignatureV2 string
136+
*/
137+
buildOrderSignature(typedData: EIP712TypedData): Promise<OrderSignatureV2> {
138+
delete typedData.types.EIP712Domain;
139+
return this.signer._signTypedData(
140+
typedData.domain,
141+
typedData.types,
142+
typedData.message,
143+
);
144+
}
145+
146+
/**
147+
* Generates the hash of the order from a EIP712TypedData object.
148+
* @param orderTypedData
149+
* @returns a OrderHashV2 string
150+
*/
151+
buildOrderHash(orderTypedData: EIP712TypedData): OrderHashV2 {
152+
const digest = hashTypedData(orderTypedData);
153+
return digest;
154+
}
155+
}

src/exchange.order-v2.const.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// V2 Exchange constants
2+
// Domain name is shared with V1; only the version changes.
3+
export const PROTOCOL_NAME_V2 = 'Polymarket CTF Exchange';
4+
export const PROTOCOL_VERSION_V2 = '2';
5+
6+
// V2 Order EIP-712 struct.
7+
// Note: `expiration` is intentionally absent — it is an API-level field,
8+
// not part of the on-chain EIP-712 signature.
9+
export const ORDER_V2_STRUCTURE = [
10+
{ name: 'salt', type: 'uint256' },
11+
{ name: 'maker', type: 'address' },
12+
{ name: 'signer', type: 'address' },
13+
{ name: 'tokenId', type: 'uint256' },
14+
{ name: 'makerAmount', type: 'uint256' },
15+
{ name: 'takerAmount', type: 'uint256' },
16+
{ name: 'side', type: 'uint8' },
17+
{ name: 'signatureType', type: 'uint8' },
18+
{ name: 'timestamp', type: 'uint256' },
19+
{ name: 'metadata', type: 'bytes32' },
20+
{ name: 'builder', type: 'bytes32' },
21+
];

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
export * from './exchange.order.builder.ts';
22
export * from './exchange.order.const.ts';
3+
export * from './exchange.order-v2.builder.ts';
4+
export * from './exchange.order-v2.const.ts';
35

46
export * from './model/abi.model.ts';
57
export * from './model/eip712.model.ts';
68
export * from './model/order.model.ts';
9+
export * from './model/order-v2.model.ts';
710
export * from './model/order-side.model.ts';
811
export * from './model/signature-types.model.ts';

src/model/order-v2.model.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import type { EIP712Object } from './eip712.model.ts';
2+
import type { Side } from './order-side.model.ts';
3+
import type { SignatureType } from './signature-types.model.ts';
4+
5+
export type OrderSignatureV2 = string;
6+
7+
export type OrderHashV2 = string;
8+
9+
export interface OrderDataV2 {
10+
/**
11+
* Maker of the order, i.e the source of funds for the order
12+
*/
13+
maker: string;
14+
15+
/**
16+
* Token Id of the CTF ERC1155 asset to be bought or sold.
17+
* If BUY, this is the tokenId of the asset to be bought, i.e the makerAssetId
18+
* If SELL, this is the tokenId of the asset to be sold, i.e the takerAssetId
19+
*/
20+
tokenId: string;
21+
22+
/**
23+
* Maker amount, i.e the max amount of tokens to be sold
24+
*/
25+
makerAmount: string;
26+
27+
/**
28+
* Taker amount, i.e the minimum amount of tokens to be received
29+
*/
30+
takerAmount: string;
31+
32+
/**
33+
* The side of the order, BUY or SELL
34+
*/
35+
side: Side;
36+
37+
/**
38+
* Signer of the order. Optional, if it is not present the signer is the maker of the order.
39+
*/
40+
signer?: string;
41+
42+
/**
43+
* Signature type used by the Order. Default value 'EOA'
44+
*/
45+
signatureType?: SignatureType;
46+
47+
/**
48+
* Timestamp of the order
49+
*/
50+
timestamp?: string;
51+
52+
/**
53+
* Metadata of the order (bytes32)
54+
*/
55+
metadata?: string;
56+
57+
/**
58+
* Builder of the order (bytes32)
59+
*/
60+
builder?: string;
61+
62+
/**
63+
* Expiration timestamp of the order (unix seconds, "0" = no expiration)
64+
*/
65+
expiration?: string;
66+
}
67+
68+
export interface OrderV2 extends EIP712Object {
69+
/**
70+
* Unique salt to ensure entropy
71+
*/
72+
readonly salt: string;
73+
74+
/**
75+
* Maker of the order, i.e the source of funds for the order
76+
*/
77+
readonly maker: string;
78+
79+
/**
80+
* Signer of the order
81+
*/
82+
readonly signer: string;
83+
84+
/**
85+
* Token Id of the CTF ERC1155 asset to be bought or sold.
86+
* If BUY, this is the tokenId of the asset to be bought, i.e the makerAssetId
87+
* If SELL, this is the tokenId of the asset to be sold, i.e the takerAssetId
88+
*/
89+
readonly tokenId: string;
90+
91+
/**
92+
* Maker amount, i.e the max amount of tokens to be sold
93+
*/
94+
readonly makerAmount: string;
95+
96+
/**
97+
* Taker amount, i.e the minimum amount of tokens to be received
98+
*/
99+
readonly takerAmount: string;
100+
101+
/**
102+
* The side of the order, BUY or SELL
103+
*/
104+
readonly side: Side;
105+
106+
/**
107+
* Signature type used by the Order
108+
*/
109+
readonly signatureType: SignatureType;
110+
111+
/**
112+
* Timestamp of the order
113+
*/
114+
readonly timestamp: string;
115+
116+
/**
117+
* Metadata of the order (bytes32)
118+
*/
119+
readonly metadata: string;
120+
121+
/**
122+
* Builder of the order (bytes32)
123+
*/
124+
readonly builder: string;
125+
126+
/**
127+
* Expiration timestamp of the order (unix seconds, "0" = no expiration)
128+
*/
129+
readonly expiration: string;
130+
}
131+
132+
export interface SignedOrderV2 extends OrderV2 {
133+
/**
134+
* The order signature
135+
*/
136+
readonly signature: OrderSignatureV2;
137+
}

0 commit comments

Comments
 (0)