Skip to content
Closed
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@polymarket/order-utils",
"version": "3.1.0",
"version": "3.2.0",
"description": "Typescript utility for creating orders for Polymarket's CLOB",
"author": "Liam Kovatch <liam@polymarket.com>",
"homepage": "https://github.com/Polymarket/clob-order-utils",
Expand Down
57 changes: 51 additions & 6 deletions src/exchange.order.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
PROTOCOL_VERSION,
} from './exchange.order.const.ts';
import type { EIP712TypedData } from './model/eip712.model.ts';
import { hashTypedData } from 'viem';
import { hashTypedData, type WalletClient } from 'viem';
import type {
Order,
OrderData,
Expand All @@ -18,13 +18,57 @@ import type {
import { SignatureType } from './model/signature-types.model.ts';
import { generateOrderSalt } from './utils.ts';

type ExchangeSignerInput = Wallet | JsonRpcSigner | WalletClient;

interface IExchangeSigner {
getAddress(): Promise<string>;
signTypedData(
domain: EIP712TypedData['domain'],
types: EIP712TypedData['types'],
value: EIP712TypedData['message'],
primaryType?: string
): Promise<OrderSignature>;
}

function createExchangeSigner(signer: ExchangeSignerInput): IExchangeSigner {
if ('_signTypedData' in signer) {
return {
getAddress: async () => signer.getAddress(),
signTypedData: async (domain, types, value) =>
signer._signTypedData(domain, types, value),
};
}

if (!signer.account) {
throw new Error('walletClient.account is required');
}

const account = signer.account;

return {
getAddress: async () => account.address,
signTypedData: async (domain, types, value, primaryType) =>
signer.signTypedData({
account,
domain,
types,
primaryType: primaryType ?? 'Order',
message: value,
}),
};
}

export class ExchangeOrderBuilder {
private readonly exchangeSigner: IExchangeSigner;

constructor(
private readonly contractAddress: string,
private readonly chainId: number,
private readonly signer: Wallet | JsonRpcSigner,
signer: ExchangeSignerInput,
private readonly generateSalt = generateOrderSalt
) {}
) {
this.exchangeSigner = createExchangeSigner(signer);
}

/**
* build an order object including the signature.
Expand Down Expand Up @@ -64,7 +108,7 @@ export class ExchangeOrderBuilder {
signer = maker;
}

const signerAddress = await this.signer.getAddress();
const signerAddress = await this.exchangeSigner.getAddress();
if (signer !== signerAddress) {
throw new Error('signer does not match');
}
Expand Down Expand Up @@ -136,10 +180,11 @@ export class ExchangeOrderBuilder {
*/
buildOrderSignature(typedData: EIP712TypedData): Promise<OrderSignature> {
delete typedData.types.EIP712Domain;
return this.signer._signTypedData(
return this.exchangeSigner.signTypedData(
typedData.domain,
typedData.types,
typedData.message
typedData.message,
typedData.primaryType
);
}

Expand Down
86 changes: 86 additions & 0 deletions tests/src/exchange.order.builder.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect } from 'chai';
import { Wallet } from '@ethersproject/wallet';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { ExchangeOrderBuilder } from '../../src/exchange.order.builder.ts';
import { generateOrderSalt } from '../../src/utils.ts';
import type { Order, OrderData } from '../../src/model/order.model.ts';
Expand Down Expand Up @@ -885,4 +887,88 @@ describe('order builder', () => {
});
});
});

describe('WalletClient signer support', () => {
const chainId = 80002;
const exchangeAddress = '0xdFE02Eb6733538f8Ea35D585af8DE5958AD99E40';
const privateKey =
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';

it('buildOrder uses walletClient account address', async () => {
const account = privateKeyToAccount(privateKey);
const walletClient = createWalletClient({
account,
transport: http('http://127.0.0.1:8545'),
});

const builder = new ExchangeOrderBuilder(
exchangeAddress,
chainId,
walletClient,
generateOrderSalt
);

const order = await builder.buildOrder({
maker: account.address,
taker: '0x0000000000000000000000000000000000000000',
tokenId: '1234',
makerAmount: '100000000',
takerAmount: '50000000',
side: Side.BUY,
feeRateBps: '100',
nonce: '0',
} as OrderData);

expect(order.signer).equal(account.address);
});

it('buildOrderSignature signs with walletClient', async () => {
const account = privateKeyToAccount(privateKey);
const walletClient = createWalletClient({
account,
transport: http('http://127.0.0.1:8545'),
});

const builder = new ExchangeOrderBuilder(
exchangeAddress,
chainId,
walletClient,
() => '479249096354'
);

const order = await builder.buildOrder({
maker: account.address,
taker: '0x0000000000000000000000000000000000000000',
tokenId: '1234',
makerAmount: '100000000',
takerAmount: '50000000',
side: Side.BUY,
feeRateBps: '100',
nonce: '0',
} as OrderData);

const orderTypedData = builder.buildOrderTypedData(order);
const signature = await builder.buildOrderSignature(orderTypedData);

expect(signature).equal(
// eslint-disable-next-line max-len
'0x302cd9abd0b5fcaa202a344437ec0b6660da984e24ae9ad915a592a90facf5a51bb8a873cd8d270f070217fea1986531d5eec66f1162a81f66e026db653bf7ce1c'
);
});

it('throws if walletClient.account is missing', () => {
const walletClient = createWalletClient({
transport: http('http://127.0.0.1:8545'),
});

expect(() => {
new ExchangeOrderBuilder(
exchangeAddress,
chainId,
walletClient,
generateOrderSalt
);
}).to.throw('walletClient.account is required');
});
});
});