import { ethers, BigNumberish } from 'ethers'; import { LimitOrderClient, Order } from './LimitOrderClient'; export interface FetchedOrder { order: Order; orderSignature: string; permit2Signature: string; orderHash: string; createdAt: number; } export interface FillResult { success: boolean; transactionHash?: string; error?: string; } export interface OrderValidation { isValid: boolean; errors: string[]; warnings: string[]; } /** * SDK for filling limit orders (Taker/Filler side) * Designed for automated scripts and bots */ export class LimitOrderFiller { private client: LimitOrderClient; private signer: ethers.Signer; private serverUrl: string; private provider: ethers.JsonRpcProvider; constructor( client: LimitOrderClient, signer: ethers.Signer, provider: ethers.JsonRpcProvider, serverUrl: string = 'https://limit-engine.deserialize.xyz/api/v1' ) { this.client = client; this.signer = signer; this.provider = provider; this.serverUrl = serverUrl; } /** * Fetch unfilled orders from the external server * @param filters Optional filters for fetching orders */ public async fetchUnfilledOrders(filters?: { makerToken?: string; takerToken?: string; minMakerAmount?: BigNumberish; maxTakerAmount?: BigNumberish; limit?: number; }): Promise { return this.fetchOrdersFromServer(filters); } /** * Fetch open orders (optimized endpoint for fillers) */ public async fetchOpenOrders(filters?: { makerToken?: string; takerToken?: string; limit?: number; }): Promise { try { const url = new URL(`${this.serverUrl}/orders/open`); if (filters?.makerToken) url.searchParams.append('makerToken', filters.makerToken); if (filters?.takerToken) url.searchParams.append('takerToken', filters.takerToken); if (filters?.limit) url.searchParams.append('limit', filters.limit.toString()); const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); return this.transformServerOrders(result.data || []); } catch (error: any) { console.error('Failed to fetch open orders:', error.message); return []; } } /** * Fetch orders by token pair */ public async fetchOrdersByTokenPair( makerToken: string, takerToken: string, limit: number = 50 ): Promise { try { const url = new URL(`${this.serverUrl}/pairs/${makerToken}/${takerToken}/orders`); url.searchParams.append('limit', limit.toString()); url.searchParams.append('notExpired', 'true'); const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); return this.transformServerOrders(result.data || []); } catch (error: any) { console.error('Failed to fetch orders by token pair:', error.message); return []; } } /** * Validate an order before filling * @param fetchedOrder The order to validate */ public async validateOrder(fetchedOrder: FetchedOrder): Promise { const errors: string[] = []; const warnings: string[] = []; try { // 1. Check if order hash matches const computedHash = await this.client.getOrderHash(fetchedOrder.order); if (computedHash !== fetchedOrder.orderHash) { errors.push(`Order hash mismatch: received hash ${fetchedOrder.orderHash} : computed hash ${computedHash}`); } // 2. Check if already filled const isFilled = await this.client.isOrderFilled(fetchedOrder.orderHash); if (isFilled) { errors.push('Order already filled'); } // 3. Check if cancelled const isCancelled = await this.client.isCancelled( fetchedOrder.order.maker, fetchedOrder.order.salt ); if (isCancelled) { errors.push('Order has been cancelled'); } // 4. Check if expired const currentTime = BigInt(Math.floor(Date.now() / 1000)); const expiry = BigInt(fetchedOrder.order.expiry); if (expiry !== BigInt(0) && currentTime > expiry) { errors.push('Order has expired'); } // 5. Validate signatures const signaturesValid = await this.client.validateOrderSignatures( fetchedOrder.order, fetchedOrder.orderSignature, fetchedOrder.permit2Signature ); if (!signaturesValid) { errors.push('Invalid signatures'); } // 6. Check maker token balance const makerBalance = await this.getTokenBalance( fetchedOrder.order.makerToken, fetchedOrder.order.maker ); if (makerBalance < BigInt(fetchedOrder.order.makerAmount.toString())) { warnings.push('Maker has insufficient balance'); } // 7. Check taker token balance (self) const takerAddress = await this.signer.getAddress(); const takerBalance = await this.getTokenBalance( fetchedOrder.order.takerToken, takerAddress ); if (takerBalance < BigInt(fetchedOrder.order.takerAmount.toString())) { errors.push('Insufficient balance to fill order'); } } catch (error: any) { errors.push(`Validation error: ${error.message}`); } return { isValid: errors.length === 0, errors, warnings }; } /** * Fill a single order * @param fetchedOrder The order to fill * @param skipValidation Skip validation (use with caution) */ public async fillOrder( fetchedOrder: FetchedOrder, skipValidation: boolean = false ): Promise { try { // Validate order first if (!skipValidation) { const validation = await this.validateOrder(fetchedOrder); if (!validation.isValid) { return { success: false, error: `Validation failed: ${validation.errors.join(', ')}` }; } if (validation.warnings.length > 0) { console.warn('āš ļø Warnings:', validation.warnings.join(', ')); } } // Ensure approval for taker token await this.ensureLimitOrderContractApproval(fetchedOrder.order.takerToken); // Build fill transaction const fillTx = this.client.getFillOrderWithPermit2Transaction( fetchedOrder.order, fetchedOrder.orderSignature, fetchedOrder.permit2Signature ); console.log('šŸ”„ Filling order:', fetchedOrder.orderHash); // Send transaction const tx = await this.signer.sendTransaction(fillTx); console.log('ā³ Transaction sent:', tx.hash); // Wait for confirmation const receipt = await tx.wait(); console.log('āœ… Order filled in block:', receipt?.blockNumber); // Notify server that order was filled try { await this.notifyServerOfFill(fetchedOrder.orderHash, tx.hash); } catch (error) { console.warn('āš ļø Failed to notify server of fill:', error); } return { success: true, transactionHash: tx.hash }; } catch (error: any) { console.error('āŒ Fill error:', error.message); return { success: false, error: error.message }; } } /** * Fill multiple orders in sequence with validation * @param orders Array of orders to fill */ public async fillMultipleOrders(orders: FetchedOrder[]): Promise<{ successful: string[]; failed: { orderHash: string; error: string }[]; }> { const successful: string[] = []; const failed: { orderHash: string; error: string }[] = []; for (const order of orders) { console.log(`\nšŸ“‹ Processing order ${order.orderHash}...`); const result = await this.fillOrder(order); if (result.success && result.transactionHash) { successful.push(result.transactionHash); } else { failed.push({ orderHash: order.orderHash, error: result.error || 'Unknown error' }); } // Small delay between orders to avoid nonce issues await new Promise(resolve => setTimeout(resolve, 2000)); } return { successful, failed }; } /** * Continuously monitor and fill orders * @param pollIntervalMs Polling interval in milliseconds * @param filters Optional filters for fetching orders */ public async startFillingLoop( pollIntervalMs: number = 10000, filters?: { makerToken?: string; takerToken?: string; minMakerAmount?: BigNumberish; maxTakerAmount?: BigNumberish; } ): Promise { console.log('šŸ¤– Starting order filling bot...'); console.log(`šŸ“Š Polling interval: ${pollIntervalMs}ms`); while (true) { try { console.log('\nšŸ” Fetching unfilled orders...'); const orders = await this.fetchOpenOrders({ makerToken: filters?.makerToken, takerToken: filters?.takerToken, limit: 100, }); console.log(`šŸ“¦ Found ${orders.length} orders`); if (orders.length > 0) { const result = await this.fillMultipleOrders(orders); console.log(`\nāœ… Successfully filled: ${result.successful.length}`); console.log(`āŒ Failed: ${result.failed.length}`); } // Wait before next poll await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); } catch (error: any) { console.error('āŒ Loop error:', error.message); // Continue loop even on error await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); } } } /** * Get token balance for an address */ async getTokenBalance(tokenAddress: string, address: string): Promise { if (tokenAddress.toLowerCase() === this.client.NATIVE_TOKEN.toLowerCase()) { return await this.provider.getBalance(address); } const ERC20_ABI = [ "function balanceOf(address owner) external view returns (uint256)" ]; const token = new ethers.Contract(tokenAddress, ERC20_ABI, this.provider); return await token.balanceOf(address); } /** * Ensure the limit order contract has approval for the taker token */ private async ensureLimitOrderContractApproval(tokenAddress: string): Promise { if (tokenAddress.toLowerCase() === this.client.NATIVE_TOKEN.toLowerCase()) { return; // No approval needed for native token } const takerAddress = await this.signer.getAddress(); const isApproved = await this.isLimitOrderContractApproved(tokenAddress); if (!isApproved) { console.log('šŸ”„ Approving limit order contract...'); const approveTx = await this.approveLimitOrderContract(tokenAddress); await approveTx.wait(); console.log('āœ… Limit order contract approved'); } } /** * Check if limit order contract is approved for a token */ private async isLimitOrderContractApproved(tokenAddress: string): Promise { const ERC20_ABI = [ "function allowance(address owner, address spender) external view returns (uint256)" ]; const token = new ethers.Contract(tokenAddress, ERC20_ABI, this.signer); const takerAddress = await this.signer.getAddress(); const allowance = await token.allowance(takerAddress, this.client.LIMIT_ORDER_CONTRACT); return allowance > 0; } /** * Approve limit order contract for a token */ private async approveLimitOrderContract(tokenAddress: string): Promise { const ERC20_ABI = [ "function approve(address spender, uint256 amount) external returns (bool)" ]; const token = new ethers.Contract(tokenAddress, ERC20_ABI, this.signer); return await token.approve(this.client.LIMIT_ORDER_CONTRACT, ethers.MaxUint256); } /** * Fetch orders from external server with filters */ private async fetchOrdersFromServer(filters?: any): Promise { try { const url = new URL(`${this.serverUrl}/orders`); // Add filters as query params url.searchParams.append('status', 'OPEN'); url.searchParams.append('notExpired', 'true'); if (filters?.makerToken) url.searchParams.append('makerToken', filters.makerToken); if (filters?.takerToken) url.searchParams.append('takerToken', filters.takerToken); if (filters?.minMakerAmount) url.searchParams.append('minMakerAmount', filters.minMakerAmount.toString()); if (filters?.maxTakerAmount) url.searchParams.append('maxTakerAmount', filters.maxTakerAmount.toString()); if (filters?.limit) url.searchParams.append('limit', filters.limit.toString()); console.log('🌐 Fetching orders from server:', url.toString()); const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); const orders = this.transformServerOrders(result.data || []); console.log(`āœ… Fetched ${orders.length} orders from server`); return orders; } catch (error: any) { console.error('Failed to fetch orders from server:', error.message); return []; } } /** * Transform server order format to FetchedOrder format */ private transformServerOrders(serverOrders: any[]): FetchedOrder[] { return serverOrders.map(serverOrder => ({ order: { maker: serverOrder.maker, makerToken: serverOrder.makerToken, takerToken: serverOrder.takerToken, makerAmount: serverOrder.makerAmount, takerAmount: serverOrder.takerAmount, salt: serverOrder.salt, expiry: BigInt(serverOrder.expiry), }, orderSignature: serverOrder.orderSignature, permit2Signature: serverOrder.permit2Signature, orderHash: serverOrder.orderHash, createdAt: new Date(serverOrder.createdAt).getTime(), })); } /** * Notify server that order was filled */ private async notifyServerOfFill(orderHash: string, transactionHash: string): Promise { try { const takerAddress = await this.signer.getAddress(); const response = await fetch(`${this.serverUrl}/orders/${orderHash}/fill`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ orderHash, taker: takerAddress, transactionHash, }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } console.log('āœ… Server notified of fill'); } catch (error: any) { console.error('Failed to notify server:', error.message); throw error; } } /** * Get the taker address */ public async getTakerAddress(): Promise { return await this.signer.getAddress(); } /** * Get the LimitOrderClient instance */ public getClient(): LimitOrderClient { return this.client; } /** * Set server URL */ public setServerUrl(url: string): void { this.serverUrl = url; } /** * Get current server URL */ public getServerUrl(): string { return this.serverUrl; } }