import { JsonRpcApiProvider, TransactionRequest } from 'ethers'; import { Contract } from 'ethers'; import { ContractRunner } from 'ethers'; import { Wallet } from 'ethers'; import { ethers, BigNumberish, Signature } from 'ethers'; import { Chain } from 'viem'; import LimitOrderAbi from "../LimitOrderABI.json" const wrappedA0GIAbi = [ "function deposit() payable", "event Deposit(address indexed dst, uint wad)" ]; /** * Order structure matching the Solidity contract */ export interface Order { maker: string; makerToken: string; takerToken: string; makerAmount: BigNumberish; takerAmount: BigNumberish; salt: BigNumberish; expiry: BigNumberish; } export interface BaseOrder extends Omit { } /** * Permit2 TokenPermissions structure */ export interface TokenPermissions { token: string; amount: BigNumberish; } /** * Permit2 PermitTransferFrom structure */ export interface PermitTransferFrom { permitted: TokenPermissions; spender: string; nonce: BigNumberish; deadline: BigNumberish; } /** * Event data structures */ export interface OrderFilledEvent { orderHash: string; maker: string; taker: string; makerAmount: bigint; takerAmount: bigint; blockNumber: number; transactionHash: string; } export interface OrderCancelledEvent { orderHash: string; maker: string; salt: bigint; blockNumber: number; transactionHash: string; } /** * Transaction options */ export interface TxOptions { gasLimit?: BigNumberish; gasPrice?: BigNumberish; maxFeePerGas?: BigNumberish; maxPriorityFeePerGas?: BigNumberish; nonce?: number; value?: BigNumberish; } export const TESTNET_CHAIN_ID = 16602 export const MAINNET_CHAIN_ID = 16601 /** * TypeScript client for interacting with the LimitOrderWithPermit2 smart contract */ export class LimitOrderClient { private contract: ethers.Contract; private provider: ethers.JsonRpcProvider; public readonly PERMIT2_CONTRACT: string; public readonly LIMIT_ORDER_CONTRACT public readonly W_NATIVE_ADDRESS public chain: Chain // Contract constants public readonly NATIVE_TOKEN = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; public readonly NAME = 'LimitOrderWithPermit2'; public readonly VERSION = '1'; // EIP-712 Domain private domain: ethers.TypedDataDomain; // EIP-712 Types private readonly ORDER_TYPES = { Order: [ { name: 'maker', type: 'address' }, { name: 'makerToken', type: 'address' }, { name: 'takerToken', type: 'address' }, { name: 'makerAmount', type: 'uint256' }, { name: 'takerAmount', type: 'uint256' }, { name: 'salt', type: 'uint256' }, { name: 'expiry', type: 'uint256' }, ], }; /** * Creates a new LimitOrderClient instance * @param provider JsonRpcProvider instance * @param contractAddress The deployed contract address * @param abi Contract ABI * @param signer Optional signer or signer index for transactions */ constructor( provider: ethers.JsonRpcProvider, contractAddress: string, abi: ethers.InterfaceAbi = LimitOrderAbi, permit2Address: string, chain: Chain, warpedNativeAddress: string ) { this.provider = provider; // Initialize contract with provider this.contract = new ethers.Contract(contractAddress, abi, provider); // Initialize EIP-712 domain (chainId will be set dynamically) this.PERMIT2_CONTRACT = permit2Address; this.LIMIT_ORDER_CONTRACT = contractAddress this.chain = chain this.W_NATIVE_ADDRESS = warpedNativeAddress this.domain = { name: this.NAME, version: this.VERSION, chainId: chain.id, verifyingContract: contractAddress, }; } // ==================== VIEW FUNCTIONS ==================== /** * Get the contract name */ public async getName(): Promise { return await this.contract.NAME(); } /** * Get the contract version */ public async getVersion(): Promise { return await this.contract.VERSION(); } /** * Get the domain separator */ public async getDomainSeparator(): Promise { return await this.contract.DOMAIN_SEPARATOR(); } /** * Get the Permit2 contract address */ public async getPermit2Address(): Promise { return await this.contract.permit2(); } /** * Check if an order has been filled * @param orderHash The hash of the order */ public async isOrderFilled(orderHash: string): Promise { return await this.contract.isOrderFilled(orderHash); } /** * Check if an order has been cancelled * @param maker The maker address * @param salt The order salt/nonce */ public async isCancelled(maker: string, salt: BigNumberish): Promise { return await this.contract.cancelled(maker, salt); } /** * Check if the contract is paused */ public async isPaused(): Promise { return await this.contract.paused(); } /** * Get the contract owner */ public async getOwner(): Promise { return await this.contract.owner(); } /** * Get the order hash for a given order * @param order The order struct */ public async getOrderHash(order: Order): Promise { return await this.contract.getOrderHash(order); } /** * Check if an order is filled by computing its hash * @param order The order struct */ public async isFilled(order: Order): Promise { const orderHash = await this.getOrderHash(order); return await this.isOrderFilled(orderHash); } public getWrapNativeTransaction = (amount: string, wNativeAddress = this.W_NATIVE_ADDRESS, wNativeAbi = wrappedA0GIAbi) => { const wNative = new ethers.Contract( wNativeAddress, wNativeAbi, ); const iface = wNative.interface const data = iface.encodeFunctionData('deposit') const value = amount const tx: TransactionRequest = { to: wNativeAddress, data, value, } return tx; } public getPermitFromOrder(order: Order): PermitTransferFrom { const permit: PermitTransferFrom = { permitted: { token: order.makerToken, amount: order.makerAmount, }, spender: this.LIMIT_ORDER_CONTRACT, nonce: order.salt, deadline: order.expiry === 0 ? ethers.MaxUint256 : order.expiry, }; return permit; } // ==================== EIP-712 SIGNING ==================== /** * Get the EIP-712 domain for signing */ private async getDomain(): Promise { return this.domain; } /** * Sign an order using EIP-712 * @param order The order to sign * @param signer Optional signer (uses default if not provided) */ public async signOrder(order: Order, signer: ethers.Signer): Promise { const signerToUse = signer const domain = this.domain const signature = await signerToUse.signTypedData(domain, this.ORDER_TYPES, order); return signature; } public getOrderDataToSign(order: Order): { domain: ethers.TypedDataDomain; types: any; value: Order } { const domain = this.domain return { domain, types: this.ORDER_TYPES, value: order, }; } /** * Verify an order signature * @param order The order struct * @param signature The signature to verify * @param expectedMaker The expected maker address */ public async verifyOrderSignature( order: Order, signature: string, expectedMaker: string ): Promise { try { const domain = await this.getDomain(); const recovered = ethers.verifyTypedData(domain, this.ORDER_TYPES, order, signature); return recovered.toLowerCase() === expectedMaker.toLowerCase(); } catch (error) { return false; } } /** * Create a Permit2 signature for token transfer * Note: This requires the maker to sign with their wallet * @param permit The permit data * @param signer The signer (maker) */ public async signPermit2( order: Order, signer: ethers.Signer ): Promise { const { domain, types, value } = this.getPermit2DataToSign(order); const signature = await signer.signTypedData(domain, types, value); return signature; } // instead of the above we get the transaction instead of having to pass in the signer public getPermit2DataToSign(order: Order): { domain: ethers.TypedDataDomain; types: any; value: PermitTransferFrom } { const permit = this.getPermitFromOrder(order); const permit2Address = this.PERMIT2_CONTRACT const network = this.chain const permit2Domain = { name: 'Permit2', chainId: Number(network.id), verifyingContract: permit2Address, }; const permit2Types = { PermitTransferFrom: [ { name: 'permitted', type: 'TokenPermissions' }, { name: "spender", type: "address" }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, ], TokenPermissions: [ { name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }, ], }; return { domain: permit2Domain, types: permit2Types, value: permit, }; } //verifyPermit2Signature public async verifyPermit2Signature( signature: string, order: Order, ): Promise { const expectedMaker = order.maker; const { domain, types, value } = this.getPermit2DataToSign(order); try { const recovered = ethers.verifyTypedData(domain, types, value, signature); if (recovered.toLowerCase() !== expectedMaker.toLowerCase()) { console.error('Invalid Permit2 signature'); return false; } } catch (error) { console.error('Error verifying Permit2 signature:', error); return false; } finally { return true; } } // ==================== WRITE FUNCTIONS ==================== // Add this debugging function to your LimitOrderClient class /** * Decode custom error from error data */ private decodeError(errorData: string): string { const errorSelectors: { [key: string]: string } = { '0x815e1d64': 'InvalidSigner()', '0xff633a38': 'LengthMismatch()', '0xd81b2f2e': 'AllowanceExpired()', '0x24d35a26': 'ExcessiveInvalidation()', '0xf96fb071': 'InsufficientAllowance()', '0x3728b83d': 'InvalidAmount', '0xb0669cbc': 'InvalidContractSignature', '0x756688fe': 'InvalidNonce', '0x8baa579f': 'InvalidSignature', 'InvalidSignatureLength': '0x4be6321b', '0xcd21db4f': 'SignatureExpired', // Add more Permit2 errors as needed }; const selector = errorData.slice(0, 10); return errorSelectors[selector] || `Unknown error: ${selector}`; } /** * Fill an order using Permit2 by manually encoding the transaction * @param signer The signer to use for the transaction * @param order The order to fill * @param makerSignature The maker's order signature * @param permit2Signature The maker's Permit2 signature * @param options Transaction options */ public async fillOrderWithPermit2( signer: ethers.Signer, order: Order, makerSignature: string, permit2Signature: string, options: TxOptions = {}, ): Promise { // Build the transaction const tx = this.getFillOrderWithPermit2Transaction( order, makerSignature, permit2Signature, options ); try { // Send the transaction const txResponse = await signer.sendTransaction(tx); console.log('Transaction sent:', txResponse.hash); return txResponse as ethers.ContractTransactionResponse; } catch (error: any) { // Try to decode the error if (error.data) { const decodedError = this.decodeError(error.data); console.error('Decoded error:', decodedError); } throw error; } } // the transaction version of the above functio, where we return a transaction request instead of sending it public getFillOrderWithPermit2Transaction( order: Order, makerSignature: string, permit2Signature: string, options: TxOptions = {}, ): ethers.TransactionRequest { // Add msg.value if takerToken is native token if (order.takerToken.toLowerCase() === this.NATIVE_TOKEN.toLowerCase()) { options.value = order.takerAmount; } // Get the contract interface const iface = this.contract.interface; // Manually encode the function call const data = iface.encodeFunctionData('fillOrderWithPermit2', [ order, makerSignature, permit2Signature ]); // Build the transaction const tx: ethers.TransactionRequest = { to: this.contract.target as string, data: data, value: options.value || 0, gasLimit: options.gasLimit, gasPrice: options.gasPrice, maxFeePerGas: options.maxFeePerGas, maxPriorityFeePerGas: options.maxPriorityFeePerGas, nonce: options.nonce, }; return tx; } /** * Cancel an order by salt * @param salt The order salt/nonce * @param options Transaction options */ public async cancelOrder( salt: BigNumberish, signer: ethers.Signer, options: TxOptions = {} ): Promise { // Connect the contract to the signer this.contract = this.contract.connect(signer.connect(this.provider)) as ethers.Contract; return await this.contract.cancelOrder(salt, options); } // transaction version of the above function public getCancelOrderTransaction( salt: BigNumberish, options: TxOptions = {} ): ethers.TransactionRequest { // Get the contract interface const iface = this.contract.interface; // Manually encode the function call const data = iface.encodeFunctionData('cancelOrder', [salt]); // Build the transaction const tx: ethers.TransactionRequest = { to: this.contract.target as string, data: data, gasLimit: options.gasLimit, gasPrice: options.gasPrice, maxFeePerGas: options.maxFeePerGas, maxPriorityFeePerGas: options.maxPriorityFeePerGas, nonce: options.nonce, }; return tx; } // ==================== EVENT QUERIES ==================== /** * Query OrderFilled events * @param fromBlock Starting block * @param toBlock Ending block * @param filters Optional filters (orderHash, maker, taker) */ public async getOrderFilledEvents( fromBlock: number | string = 0, toBlock: number | string = 'latest', filters: { orderHash?: string; maker?: string; taker?: string } = {} ): Promise { const eventFilter = this.contract.filters.OrderFilled( filters.orderHash, filters.maker, filters.taker ); const events = await this.contract.queryFilter(eventFilter, fromBlock, toBlock); console.log('events: ', events); return events.map((event) => ({ orderHash: event.toJSON().args![0] as string, maker: event.toJSON().args![1] as string, taker: event.toJSON().args![2] as string, makerAmount: event.toJSON().args![3] as bigint, takerAmount: event.toJSON().args![4] as bigint, blockNumber: event.blockNumber, transactionHash: event.transactionHash, })); } /** * Query OrderCancelled events * @param fromBlock Starting block * @param toBlock Ending block * @param filters Optional filters (orderHash, maker) */ public async getOrderCancelledEvents( fromBlock: number | string = 0, toBlock: number | string = 'latest', filters: { orderHash?: string; maker?: string } = {} ): Promise { const eventFilter = this.contract.filters.OrderCancelled(filters.orderHash, filters.maker); const events = await this.contract.queryFilter(eventFilter, fromBlock, toBlock); console.log('events: ', events); return events.map((event) => ({ orderHash: event.toJSON().args![0] as string, maker: event.toJSON().args![1] as string, salt: event.toJSON().args![2] as bigint, blockNumber: event.blockNumber, transactionHash: event.transactionHash, })); } /** * Listen for OrderFilled events * @param callback Callback function * @param filters Optional filters */ public onOrderFilled( callback: (event: OrderFilledEvent) => void, filters: { orderHash?: string; maker?: string; taker?: string } = {} ): void { const eventFilter = this.contract.filters.OrderFilled( filters.orderHash, filters.maker, filters.taker ); this.contract.on(eventFilter, (orderHash, maker, taker, makerAmount, takerAmount, event) => { callback({ orderHash, maker, taker, makerAmount, takerAmount, blockNumber: event.log.blockNumber, transactionHash: event.log.transactionHash, }); }); } /** * Listen for OrderCancelled events * @param callback Callback function * @param filters Optional filters */ public onOrderCancelled( callback: (event: OrderCancelledEvent) => void, filters: { orderHash?: string; maker?: string } = {} ): void { const eventFilter = this.contract.filters.OrderCancelled(filters.orderHash, filters.maker); this.contract.on(eventFilter, (orderHash, maker, salt, event) => { callback({ orderHash, maker, salt, blockNumber: event.log.blockNumber, transactionHash: event.log.transactionHash, }); }); } /** * Remove all event listeners */ public removeAllListeners(): void { this.contract.removeAllListeners(); } // ==================== UTILITY FUNCTIONS ==================== /** * Create a complete order with both signatures * @param orderParams Order parameters * @param makerSigner The maker's signer */ public async createSignedOrder( orderParams: Order, makerSigner: ethers.Signer ): Promise<{ order: Order; orderSignature: string; permit2Signature: string; orderHash: string; }> { // Sign the order const orderSignature = await this.signOrder(orderParams, makerSigner); // Sign the Permit2 permit const permit2Signature = await this.signPermit2(orderParams, makerSigner); // Get order hash const orderHash = await this.getOrderHash(orderParams); return { order: orderParams, orderSignature, permit2Signature, orderHash, }; } public async validateOrderSignatures( order: Order, orderSignature: string, permit2Signature: string ): Promise { const expectedMaker = order.maker; // Verify order signature const isOrderValid = await this.verifyOrderSignature(order, orderSignature, expectedMaker); if (!isOrderValid) { console.error('Invalid order signature'); return false; } // Verify Permit2 signature const isPermit2Valid = await this.verifyPermit2Signature(permit2Signature, order); if (!isPermit2Valid) { console.error('Invalid Permit2 signature'); return false; } return true; } public getCreateOrderMessageData( orderParams: Order ): { order: Order; orderDataToSign: { domain: ethers.TypedDataDomain; types: any; value: Order; }; permit2DataToSign: { domain: ethers.TypedDataDomain; types: any; value: PermitTransferFrom; }; } { // Get order data to sign const orderDataToSign = this.getOrderDataToSign(orderParams); // Get Permit2 data to sign const permit2DataToSign = this.getPermit2DataToSign(orderParams); return { order: orderParams, orderDataToSign, permit2DataToSign, }; } /** * Estimate gas for filling an order * @param order The order to fill * @param makerSignature The maker's signature * @param usePermit2 Whether to use Permit2 * @param permit2Signature Optional Permit2 signature */ public async estimateFillGas( order: Order, makerSignature: string, usePermit2: boolean = false, permit2Signature?: string ): Promise { const overrides: TxOptions = {}; if (order.takerToken.toLowerCase() === this.NATIVE_TOKEN.toLowerCase()) { overrides.value = order.takerAmount; } if (usePermit2) { if (!permit2Signature) { throw new Error('Permit2 signature required for Permit2 fill'); } return await this.contract.fillOrderWithPermit2.estimateGas( order, makerSignature, permit2Signature, overrides ); } else { return await this.contract.fillOrder.estimateGas(order, makerSignature, overrides); } } /** * Get the contract address */ public getAddress(): string { return this.contract.target as string; } /** * Get the underlying ethers contract instance */ public getContract(): ethers.Contract { return this.contract; } } // ==================== HELPER FUNCTIONS ==================== /** * Generate a random salt for an order */ export function generateOrderSalt(): bigint { return BigInt(ethers.hexlify(ethers.randomBytes(32))); } export function ensureAllowanceForLimitOrderContract(signer: ethers.Signer, tokenAddress: string, limitOrderContractAddress: string, amount: BigNumberish = ethers.MaxUint256) { const ERC20_ABI = [ "function approve(address spender, uint256 amount) external returns (bool)", "function allowance(address owner, address spender) external view returns (uint256)", "function symbol() view returns (string)" ]; return new Promise(async (resolve, reject) => { try { const token = new ethers.Contract(tokenAddress, ERC20_ABI, signer); const [owner, symbol] = await Promise.all([ signer.getAddress(), token.symbol().catch(() => "ERC20"), ]); const currentAllowance = await token.allowance(owner, limitOrderContractAddress); if (currentAllowance >= amount) { console.log(`✅ LimitOrder contract already has ${amount} approval for ${symbol}`); return resolve(null); } console.log( `🔄 Current allowance for ${symbol}: ${ethers.formatUnits( currentAllowance, 18 )}` ); console.log(`Granting unlimited approval to LimitOrder contract...`); const tx = await token.approve(limitOrderContractAddress, amount); console.log(`⏳ Approval tx sent: ${tx.hash}`); const receipt = await tx.wait(); console.log( `✅ LimitOrder contract unlimited approval confirmed for ${symbol} in block ${receipt.blockNumber}` ); return resolve(receipt); } catch (err: any) { console.error("❌ Approval check/transaction failed:", err.message); return reject(err); } }) } /** * Create an order expiry timestamp * @param durationInSeconds Duration from now in seconds */ export function createOrderExpiry(durationInSeconds: number): bigint { return BigInt(Math.floor(Date.now() / 1000) + durationInSeconds); } /** * Check if an order has expired * @param expiry The order expiry timestamp */ export function isOrderExpired(expiry: BigNumberish): boolean { const expiryNum = BigInt(expiry); if (expiryNum === BigInt(0)) return false; // No expiry return BigInt(Math.floor(Date.now() / 1000)) > expiryNum; } /** * Calculate the exchange rate (maker amount per taker amount) * @param order The order */ export function calculateExchangeRate(order: Order): string { const makerAmount = BigInt(order.makerAmount.toString()); const takerAmount = BigInt(order.takerAmount.toString()); if (takerAmount === BigInt(0)) { throw new Error('Taker amount cannot be zero'); } // Return as string to avoid precision loss return ethers.formatUnits(makerAmount * BigInt(1e18) / takerAmount, 18); } export { LimitOrderAbi }