diff --git a/src/exchange.order-v2.builder.ts b/src/exchange.order-v2.builder.ts new file mode 100644 index 0000000..562f86a --- /dev/null +++ b/src/exchange.order-v2.builder.ts @@ -0,0 +1,155 @@ +import type { JsonRpcSigner } from '@ethersproject/providers'; +import type { Wallet } from '@ethersproject/wallet'; +import { + EIP712_DOMAIN, + PROTOCOL_NAME, +} from './exchange.order.const.ts'; +import { + PROTOCOL_VERSION_V2, + ORDER_V2_STRUCTURE, +} from './exchange.order-v2.const.ts'; +import type { EIP712TypedData } from './model/eip712.model.ts'; +import { hashTypedData } from 'viem'; +import type { + OrderV2, + OrderDataV2, + OrderHashV2, + OrderSignatureV2, + SignedOrderV2, +} from './model/order-v2.model.ts'; +import { SignatureType } from './model/signature-types.model.ts'; +import { generateOrderSalt } from './utils.ts'; + +const BYTES32_ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000'; + +export class ExchangeOrderBuilderV2 { + constructor( + private readonly contractAddress: string, + private readonly chainId: number, + private readonly signer: Wallet | JsonRpcSigner, + private readonly generateSalt = generateOrderSalt, + ) {} + + /** + * Build an order object including the signature. + * @param orderData + * @returns a SignedOrderV2 object (order + signature) + */ + async buildSignedOrder(orderData: OrderDataV2): Promise { + const order = await this.buildOrder(orderData); + const orderTypedData = this.buildOrderTypedData(order); + const orderSignature = await this.buildOrderSignature(orderTypedData); + + return { + ...order, + signature: orderSignature, + } as SignedOrderV2; + } + + /** + * Creates an OrderV2 object from order data. + * @param orderData + * @returns an OrderV2 object (not signed) + */ + async buildOrder({ + maker, + tokenId, + makerAmount, + takerAmount, + side, + signer, + signatureType, + timestamp, + metadata, + builder, + expiration, + }: OrderDataV2): Promise { + if (typeof signer == 'undefined' || !signer) { + signer = maker; + } + + const resolvedSignatureType = signatureType ?? SignatureType.EOA; + + // For POLY_DEPOSIT_WALLET, the order's signer field is the wallet + // contract address, while the actual ECDSA signing is done by the EOA owner + if (resolvedSignatureType !== SignatureType.POLY_DEPOSIT_WALLET) { + const signerAddress = await this.signer.getAddress(); + if (signer !== signerAddress) { + throw new Error('signer does not match'); + } + } + + return { + salt: this.generateSalt(), + maker, + signer, + tokenId, + makerAmount, + takerAmount, + side, + signatureType: resolvedSignatureType, + timestamp: timestamp ?? Date.now().toString(), + metadata: metadata ?? BYTES32_ZERO, + builder: builder ?? BYTES32_ZERO, + expiration: expiration ?? '0', + }; + } + + /** + * Parses an OrderV2 object to EIP712 typed data + * @param order + * @returns a EIP712TypedData object + */ + buildOrderTypedData(order: OrderV2): EIP712TypedData { + return { + primaryType: 'Order', + types: { + EIP712Domain: EIP712_DOMAIN, + Order: ORDER_V2_STRUCTURE, + }, + domain: { + name: PROTOCOL_NAME, + version: PROTOCOL_VERSION_V2, + chainId: this.chainId, + verifyingContract: this.contractAddress, + }, + message: { + salt: order.salt, + maker: order.maker, + signer: order.signer, + tokenId: order.tokenId, + makerAmount: order.makerAmount, + takerAmount: order.takerAmount, + side: order.side, + signatureType: order.signatureType, + timestamp: order.timestamp, + metadata: order.metadata, + builder: order.builder, + }, + }; + } + + /** + * Generates order's signature from a EIP712TypedData object + the signer address + * @param typedData + * @returns a OrderSignatureV2 string + */ + buildOrderSignature(typedData: EIP712TypedData): Promise { + delete typedData.types.EIP712Domain; + return this.signer._signTypedData( + typedData.domain, + typedData.types, + typedData.message, + ); + } + + /** + * Generates the hash of the order from a EIP712TypedData object. + * @param orderTypedData + * @returns a OrderHashV2 string + */ + buildOrderHash(orderTypedData: EIP712TypedData): OrderHashV2 { + const digest = hashTypedData(orderTypedData); + return digest; + } +} diff --git a/src/exchange.order-v2.const.ts b/src/exchange.order-v2.const.ts new file mode 100644 index 0000000..24f84d4 --- /dev/null +++ b/src/exchange.order-v2.const.ts @@ -0,0 +1,21 @@ +// V2 Exchange constants +// Domain name is shared with V1; only the version changes. +export const PROTOCOL_NAME_V2 = 'Polymarket CTF Exchange'; +export const PROTOCOL_VERSION_V2 = '2'; + +// V2 Order EIP-712 struct. +// Note: `expiration` is intentionally absent — it is an API-level field, +// not part of the on-chain EIP-712 signature. +export const ORDER_V2_STRUCTURE = [ + { name: 'salt', type: 'uint256' }, + { name: 'maker', type: 'address' }, + { name: 'signer', type: 'address' }, + { name: 'tokenId', type: 'uint256' }, + { name: 'makerAmount', type: 'uint256' }, + { name: 'takerAmount', type: 'uint256' }, + { name: 'side', type: 'uint8' }, + { name: 'signatureType', type: 'uint8' }, + { name: 'timestamp', type: 'uint256' }, + { name: 'metadata', type: 'bytes32' }, + { name: 'builder', type: 'bytes32' }, +]; diff --git a/src/index.ts b/src/index.ts index 9dc7de4..be99587 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,11 @@ export * from './exchange.order.builder.ts'; export * from './exchange.order.const.ts'; +export * from './exchange.order-v2.builder.ts'; +export * from './exchange.order-v2.const.ts'; export * from './model/abi.model.ts'; export * from './model/eip712.model.ts'; export * from './model/order.model.ts'; +export * from './model/order-v2.model.ts'; export * from './model/order-side.model.ts'; export * from './model/signature-types.model.ts'; diff --git a/src/model/order-v2.model.ts b/src/model/order-v2.model.ts new file mode 100644 index 0000000..c729690 --- /dev/null +++ b/src/model/order-v2.model.ts @@ -0,0 +1,137 @@ +import type { EIP712Object } from './eip712.model.ts'; +import type { Side } from './order-side.model.ts'; +import type { SignatureType } from './signature-types.model.ts'; + +export type OrderSignatureV2 = string; + +export type OrderHashV2 = string; + +export interface OrderDataV2 { + /** + * Maker of the order, i.e the source of funds for the order + */ + maker: string; + + /** + * Token Id of the CTF ERC1155 asset to be bought or sold. + * If BUY, this is the tokenId of the asset to be bought, i.e the makerAssetId + * If SELL, this is the tokenId of the asset to be sold, i.e the takerAssetId + */ + tokenId: string; + + /** + * Maker amount, i.e the max amount of tokens to be sold + */ + makerAmount: string; + + /** + * Taker amount, i.e the minimum amount of tokens to be received + */ + takerAmount: string; + + /** + * The side of the order, BUY or SELL + */ + side: Side; + + /** + * Signer of the order. Optional, if it is not present the signer is the maker of the order. + */ + signer?: string; + + /** + * Signature type used by the Order. Default value 'EOA' + */ + signatureType?: SignatureType; + + /** + * Timestamp of the order + */ + timestamp?: string; + + /** + * Metadata of the order (bytes32) + */ + metadata?: string; + + /** + * Builder of the order (bytes32) + */ + builder?: string; + + /** + * Expiration timestamp of the order (unix seconds, "0" = no expiration) + */ + expiration?: string; +} + +export interface OrderV2 extends EIP712Object { + /** + * Unique salt to ensure entropy + */ + readonly salt: string; + + /** + * Maker of the order, i.e the source of funds for the order + */ + readonly maker: string; + + /** + * Signer of the order + */ + readonly signer: string; + + /** + * Token Id of the CTF ERC1155 asset to be bought or sold. + * If BUY, this is the tokenId of the asset to be bought, i.e the makerAssetId + * If SELL, this is the tokenId of the asset to be sold, i.e the takerAssetId + */ + readonly tokenId: string; + + /** + * Maker amount, i.e the max amount of tokens to be sold + */ + readonly makerAmount: string; + + /** + * Taker amount, i.e the minimum amount of tokens to be received + */ + readonly takerAmount: string; + + /** + * The side of the order, BUY or SELL + */ + readonly side: Side; + + /** + * Signature type used by the Order + */ + readonly signatureType: SignatureType; + + /** + * Timestamp of the order + */ + readonly timestamp: string; + + /** + * Metadata of the order (bytes32) + */ + readonly metadata: string; + + /** + * Builder of the order (bytes32) + */ + readonly builder: string; + + /** + * Expiration timestamp of the order (unix seconds, "0" = no expiration) + */ + readonly expiration: string; +} + +export interface SignedOrderV2 extends OrderV2 { + /** + * The order signature + */ + readonly signature: OrderSignatureV2; +} diff --git a/src/model/signature-types.model.ts b/src/model/signature-types.model.ts index d8e4363..4188995 100644 --- a/src/model/signature-types.model.ts +++ b/src/model/signature-types.model.ts @@ -13,4 +13,10 @@ export enum SignatureType { * EIP712 signatures signed by EOAs that own Polymarket Gnosis safes */ POLY_GNOSIS_SAFE, + + /** + * EIP712 signatures signed by EOAs that own Polymarket Deposit Wallets. + * Validated on-chain via ERC-1271 isValidSignature() with ERC-7739 nested domain wrapping. + */ + POLY_DEPOSIT_WALLET, }