Skip to content

Commit 136452e

Browse files
committed
Add WalletClient support to ExchangeOrderBuilder
1 parent 5fcb9a7 commit 136452e

3 files changed

Lines changed: 138 additions & 7 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@polymarket/order-utils",
3-
"version": "3.1.0",
3+
"version": "3.2.0",
44
"description": "Typescript utility for creating orders for Polymarket's CLOB",
55
"author": "Liam Kovatch <liam@polymarket.com>",
66
"homepage": "https://github.com/Polymarket/clob-order-utils",

src/exchange.order.builder.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
PROTOCOL_VERSION,
88
} from './exchange.order.const.ts';
99
import type { EIP712TypedData } from './model/eip712.model.ts';
10-
import { hashTypedData } from 'viem';
10+
import { hashTypedData, type WalletClient } from 'viem';
1111
import type {
1212
Order,
1313
OrderData,
@@ -18,13 +18,57 @@ import type {
1818
import { SignatureType } from './model/signature-types.model.ts';
1919
import { generateOrderSalt } from './utils.ts';
2020

21+
type ExchangeSignerInput = Wallet | JsonRpcSigner | WalletClient;
22+
23+
interface IExchangeSigner {
24+
getAddress(): Promise<string>;
25+
signTypedData(
26+
domain: EIP712TypedData['domain'],
27+
types: EIP712TypedData['types'],
28+
value: EIP712TypedData['message'],
29+
primaryType?: string
30+
): Promise<OrderSignature>;
31+
}
32+
33+
function createExchangeSigner(signer: ExchangeSignerInput): IExchangeSigner {
34+
if ('_signTypedData' in signer) {
35+
return {
36+
getAddress: async () => signer.getAddress(),
37+
signTypedData: async (domain, types, value) =>
38+
signer._signTypedData(domain, types, value),
39+
};
40+
}
41+
42+
if (!signer.account) {
43+
throw new Error('walletClient.account is required');
44+
}
45+
46+
const account = signer.account;
47+
48+
return {
49+
getAddress: async () => account.address,
50+
signTypedData: async (domain, types, value, primaryType) =>
51+
signer.signTypedData({
52+
account,
53+
domain,
54+
types,
55+
primaryType: primaryType ?? 'Order',
56+
message: value,
57+
}),
58+
};
59+
}
60+
2161
export class ExchangeOrderBuilder {
62+
private readonly exchangeSigner: IExchangeSigner;
63+
2264
constructor(
2365
private readonly contractAddress: string,
2466
private readonly chainId: number,
25-
private readonly signer: Wallet | JsonRpcSigner,
67+
signer: ExchangeSignerInput,
2668
private readonly generateSalt = generateOrderSalt
27-
) {}
69+
) {
70+
this.exchangeSigner = createExchangeSigner(signer);
71+
}
2872

2973
/**
3074
* build an order object including the signature.
@@ -64,7 +108,7 @@ export class ExchangeOrderBuilder {
64108
signer = maker;
65109
}
66110

67-
const signerAddress = await this.signer.getAddress();
111+
const signerAddress = await this.exchangeSigner.getAddress();
68112
if (signer !== signerAddress) {
69113
throw new Error('signer does not match');
70114
}
@@ -136,10 +180,11 @@ export class ExchangeOrderBuilder {
136180
*/
137181
buildOrderSignature(typedData: EIP712TypedData): Promise<OrderSignature> {
138182
delete typedData.types.EIP712Domain;
139-
return this.signer._signTypedData(
183+
return this.exchangeSigner.signTypedData(
140184
typedData.domain,
141185
typedData.types,
142-
typedData.message
186+
typedData.message,
187+
typedData.primaryType
143188
);
144189
}
145190

tests/src/exchange.order.builder.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { expect } from 'chai';
22
import { Wallet } from '@ethersproject/wallet';
3+
import { createWalletClient, http } from 'viem';
4+
import { privateKeyToAccount } from 'viem/accounts';
35
import { ExchangeOrderBuilder } from '../../src/exchange.order.builder.ts';
46
import { generateOrderSalt } from '../../src/utils.ts';
57
import type { Order, OrderData } from '../../src/model/order.model.ts';
@@ -885,4 +887,88 @@ describe('order builder', () => {
885887
});
886888
});
887889
});
890+
891+
describe('WalletClient signer support', () => {
892+
const chainId = 80002;
893+
const exchangeAddress = '0xdFE02Eb6733538f8Ea35D585af8DE5958AD99E40';
894+
const privateKey =
895+
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
896+
897+
it('buildOrder uses walletClient account address', async () => {
898+
const account = privateKeyToAccount(privateKey);
899+
const walletClient = createWalletClient({
900+
account,
901+
transport: http('http://127.0.0.1:8545'),
902+
});
903+
904+
const builder = new ExchangeOrderBuilder(
905+
exchangeAddress,
906+
chainId,
907+
walletClient,
908+
generateOrderSalt
909+
);
910+
911+
const order = await builder.buildOrder({
912+
maker: account.address,
913+
taker: '0x0000000000000000000000000000000000000000',
914+
tokenId: '1234',
915+
makerAmount: '100000000',
916+
takerAmount: '50000000',
917+
side: Side.BUY,
918+
feeRateBps: '100',
919+
nonce: '0',
920+
} as OrderData);
921+
922+
expect(order.signer).equal(account.address);
923+
});
924+
925+
it('buildOrderSignature signs with walletClient', async () => {
926+
const account = privateKeyToAccount(privateKey);
927+
const walletClient = createWalletClient({
928+
account,
929+
transport: http('http://127.0.0.1:8545'),
930+
});
931+
932+
const builder = new ExchangeOrderBuilder(
933+
exchangeAddress,
934+
chainId,
935+
walletClient,
936+
() => '479249096354'
937+
);
938+
939+
const order = await builder.buildOrder({
940+
maker: account.address,
941+
taker: '0x0000000000000000000000000000000000000000',
942+
tokenId: '1234',
943+
makerAmount: '100000000',
944+
takerAmount: '50000000',
945+
side: Side.BUY,
946+
feeRateBps: '100',
947+
nonce: '0',
948+
} as OrderData);
949+
950+
const orderTypedData = builder.buildOrderTypedData(order);
951+
const signature = await builder.buildOrderSignature(orderTypedData);
952+
953+
expect(signature).equal(
954+
// eslint-disable-next-line max-len
955+
'0x302cd9abd0b5fcaa202a344437ec0b6660da984e24ae9ad915a592a90facf5a51bb8a873cd8d270f070217fea1986531d5eec66f1162a81f66e026db653bf7ce1c'
956+
);
957+
});
958+
959+
it('throws if walletClient.account is missing', () => {
960+
const walletClient = createWalletClient({
961+
transport: http('http://127.0.0.1:8545'),
962+
});
963+
964+
expect(() => {
965+
new ExchangeOrderBuilder(
966+
exchangeAddress,
967+
chainId,
968+
walletClient,
969+
generateOrderSalt
970+
);
971+
}).to.throw('walletClient.account is required');
972+
});
973+
});
888974
});

0 commit comments

Comments
 (0)