Skip to content
Merged
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@
},
"dependencies": {
"@catalyst-team/cache": "^0.2.0",
"@polymarket/builder-relayer-client": "^0.0.8",
"@polymarket/builder-signing-sdk": "^0.0.8",
"@polymarket/clob-client-v2": "1.0.3",
"@polymarket/builder-relayer-client": "0.0.9",
"@polymarket/builder-signing-sdk": "1.0.0",
"@polymarket/clob-client-v2": "1.0.6",
"@types/ws": "^8.18.1",
"bottleneck": "^2.19.5",
"ethers": "5",
Expand Down
86 changes: 24 additions & 62 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

141 changes: 141 additions & 0 deletions src/__tests__/unit/trading-service-presign.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { describe, it, expect, vi } from 'vitest';
import { TradingService } from '../../services/trading-service.js';
import { RateLimiter } from '../../core/rate-limiter.js';
import { createUnifiedCache } from '../../core/unified-cache.js';

const VALID_PK = '0x' + '1'.repeat(64);
const VALID_BUILDER_CODE = '0x' + 'c'.repeat(64);

function makeService(fakeClient: Record<string, unknown>) {
const service = new TradingService(new RateLimiter(), createUnifiedCache(), {
privateKey: VALID_PK,
builderCode: VALID_BUILDER_CODE,
safeAddress: '0x' + 'a'.repeat(40),
});
(service as any).initialized = true;
(service as any).clobClient = fakeClient;
return service;
}

describe('TradingService presigned orders', () => {
it('presignMarketOrder creates a signed payload without posting', async () => {
const createMarketOrder = vi.fn(async () => ({ salt: 'signed' }));
const postOrder = vi.fn();
const service = makeService({ createMarketOrder, postOrder });

const presigned = await service.presignMarketOrder({
tokenId: 'token-1',
side: 'BUY',
amount: 3,
price: 0.5,
orderType: 'FAK',
tickSize: '0.001',
negRisk: false,
}, { key: 'key-1', ttlMs: 250 });

expect(presigned.key).toBe('key-1');
expect(presigned.kind).toBe('market');
expect(presigned.orderType).toBe('FAK');
expect(presigned.fingerprint.walletMode).toBe('builder_safe');
expect(presigned.fingerprint.maker).toBe('0x' + 'a'.repeat(40));
expect(createMarketOrder).toHaveBeenCalledTimes(1);
expect(postOrder).not.toHaveBeenCalled();
});

it('postPresignedOrder posts exactly the provided signed payload once', async () => {
const signedOrder = { salt: 'signed' };
const postOrder = vi.fn(async () => ({
success: true,
orderID: 'clob-order-1',
status: 'matched',
}));
const service = makeService({ postOrder });

const result = await service.postPresignedOrder({
key: 'key-1',
kind: 'market',
orderType: 'FAK',
signedOrder: signedOrder as any,
fingerprint: {
tokenId: 'token-1',
side: 'BUY',
orderType: 'FAK',
amount: 3,
walletMode: 'builder_safe',
maker: '0x' + 'a'.repeat(40),
signer: '0x' + 'b'.repeat(40),
signatureType: 2,
},
createdAt: Date.now(),
telemetry: {
startedAt: Date.now(),
totalMs: 0,
},
});

expect(result.success).toBe(true);
expect(result.orderId).toBe('clob-order-1');
expect(result.telemetry?.presignHit).toBe(true);
expect(result.telemetry?.signAndBuildMs).toBe(0);
expect(postOrder).toHaveBeenCalledWith(signedOrder, 'FAK');
});

it('blocks duplicate post of the same presigned object before CLOB', async () => {
const postOrder = vi.fn(async () => ({ success: true, orderID: 'clob-order-1' }));
const service = makeService({ postOrder });
const presigned = {
key: 'key-1',
kind: 'market' as const,
orderType: 'FAK' as const,
signedOrder: { salt: 'signed' } as any,
fingerprint: {
tokenId: 'token-1',
side: 'BUY' as const,
orderType: 'FAK' as const,
amount: 3,
walletMode: 'builder_safe',
maker: '0x' + 'a'.repeat(40),
signer: '0x' + 'b'.repeat(40),
signatureType: 2,
},
createdAt: Date.now(),
telemetry: { startedAt: Date.now(), totalMs: 0 },
};

await service.postPresignedOrder(presigned);
const second = await service.postPresignedOrder(presigned);

expect(second.success).toBe(false);
expect(second.errorMsg).toMatch(/already been used/);
expect(postOrder).toHaveBeenCalledTimes(1);
});

it('fingerprints deposit-wallet identity with POLY_1271 maker/funder', () => {
const depositWallet = '0x' + 'd'.repeat(40);
const service = new TradingService(new RateLimiter(), createUnifiedCache(), {
privateKey: VALID_PK,
builderCode: VALID_BUILDER_CODE,
walletMode: 'deposit_wallet',
signatureType: 3,
funderAddress: depositWallet,
});

const identity = service.getWalletIdentity();

expect(identity.walletMode).toBe('deposit_wallet');
expect(identity.maker).toBe(depositWallet);
expect(identity.funder).toBe(depositWallet);
expect(identity.signatureType).toBe(3);
expect(identity.builderCode).toBe(VALID_BUILDER_CODE);
});

it('rejects deposit-wallet mode without an explicit maker/funder', () => {
const service = new TradingService(new RateLimiter(), createUnifiedCache(), {
privateKey: VALID_PK,
builderCode: VALID_BUILDER_CODE,
walletMode: 'deposit_wallet',
});

expect(() => service.getWalletIdentity()).toThrow(/deposit_wallet wallet mode requires funderAddress/);
});
});
15 changes: 7 additions & 8 deletions src/__tests__/unit/v2-relayer-token-routing.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* Unit tests for V2 Relayer collateral-token routing.
*
* After the 2026-04-28 V2 cutover, trading collateral is `pUSD`. The
* trading-related relayer helpers (`split`, `merge`, `redeem`, `redeemBatch`)
* hardcode pUSD as the on-chain collateral argument. The off-exchange
* After the 2026-04-28 V2 cutover, CLOB trading balance is `pUSD`, but
* standard CTF split/merge/redeem calls still use the underlying `USDC.e`
* collateral. The off-exchange
* helpers (`approveUsdc`, `transferUsdc`) accept a `CollateralToken`
* parameter so they can route to either pUSD (V2 trading collateral, the
* default) or USDC.e (Onramp approval / fund-out collect path).
Expand Down Expand Up @@ -144,7 +144,7 @@ describe('RelayerService.transferUsdc — token routing', () => {
});

// ---------------------------------------------------------------------------
// split / merge — pUSD only (V2 trading collateral)
// split / merge — CTF collateral routing
// ---------------------------------------------------------------------------

describe('RelayerService.split — pUSD-only collateral', () => {
Expand All @@ -164,16 +164,15 @@ describe('RelayerService.split — pUSD-only collateral', () => {
});
});

describe('RelayerService.merge — pUSD-only collateral', () => {
it('encodes pUSD as the mergePositions collateral arg (V2)', async () => {
describe('RelayerService.merge — standard CTF collateral', () => {
it('encodes USDC.e as the mergePositions collateral arg (V2 CTF)', async () => {
const svc = makeService();
const r = await svc.merge(TEST_CONDITION_ID, '10');
expect(r.success).toBe(true);
expect(capturedExecute!.txs[0].to).toBe(CTF_CONTRACT);

const decoded = CTF_IFACE.decodeFunctionData('mergePositions', capturedExecute!.txs[0].data);
// See split note above re: pUSD vs USDC.e.
expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.pUSD.toLowerCase());
expect(decoded.collateralToken.toLowerCase()).toBe(POLYGON_CONTRACTS_V2.usdcE.toLowerCase());
});
});

Expand Down
Loading
Loading