diff --git a/src/call.ts b/src/call.ts index d3a6d0f..383f262 100644 --- a/src/call.ts +++ b/src/call.ts @@ -319,3 +319,101 @@ export const limitOrder = async ( ) } } + +/** + * Executes a market order on the specified chain for trading tokens. + * + * @param {CHAIN_IDS} chainId The chain ID. + * @param {`0x${string}`} userAddress The Ethereum address of the user placing the order. + * @param {`0x${string}`} inputToken The address of the token to be used as input. + * @param {`0x${string}`} outputToken The address of the token to be received as output. + * @param {string} amount The amount of input tokens for the order. + * @param {Object} [options] Optional parameters for the market order. + * @param {PermitSignature} [options.signature] The permit signature for token approval. + * @param {string} [options.rpcUrl] The RPC URL to use for executing the order. + * @returns {Promise} Promise resolving once the market order is executed. + */ + +export const marketOrder = async ( + chainId: CHAIN_IDS, + userAddress: `0x${string}`, + inputToken: `0x${string}`, + outputToken: `0x${string}`, + amount: string, + options?: { + signature?: PermitSignature + rpcUrl?: string + }, +): Promise => { + const { signature, rpcUrl } = options || { + signature: undefined, + rpcUrl: undefined, + } + const market = await fetchMarket(chainId, [inputToken, outputToken], rpcUrl) + const isBid = isAddressEqual(market.quote.address, inputToken) + if ((isBid && !market.bidBookOpen) || (!isBid && !market.askBookOpen)) { + throw new Error(` + import { openMarket } from '@clober-dex/v2-sdk' + + const transaction = await openMarket( + ${chainId}, + '${inputToken}', + '${outputToken}', + ) + `) + } + + const tokensToSettle = [inputToken, outputToken].filter( + (address) => !isAddressEqual(address, zeroAddress), + ) + const quoteAmount = parseUnits( + amount, + isBid ? market.quote.decimals : market.base.decimals, + ) + const { result } = await getExpectedOutput( + chainId, + inputToken, + outputToken, + amount, + rpcUrl + ? { + rpcUrl, + } + : {}, + ) + const isETH = isAddressEqual(inputToken, zeroAddress) + const permitParamsList = + signature && !isETH + ? [ + { + token: inputToken, + permitAmount: quoteAmount, + signature, + }, + ] + : [] + + return buildTransaction( + chainId, + { + chain: CHAIN_MAP[chainId], + account: userAddress, + address: CONTRACT_ADDRESSES[chainId]!.Controller, + abi: CONTROLLER_ABI, + functionName: 'take', + args: [ + result.map(({ bookId, takenAmount }) => ({ + id: bookId, + limitPrice: 0n, + quoteAmount: takenAmount, + hookData: zeroHash, + })), + tokensToSettle, + permitParamsList, + getDeadlineTimestampInSeconds(), + ], + value: isETH ? quoteAmount : 0n, + }, + options?.rpcUrl, + ) +} diff --git a/test/market-order.test.ts b/test/market-order.test.ts new file mode 100644 index 0000000..48cfe08 --- /dev/null +++ b/test/market-order.test.ts @@ -0,0 +1,146 @@ +import { afterEach, expect, test } from 'vitest' +import { marketOrder, signERC20Permit } from '@clober-dex/v2-sdk' +import { mnemonicToAccount } from 'viem/accounts' + +import { cloberTestChain } from './utils/test-chain' +import { createProxyClients } from './utils/utils' +import { FORK_URL, TEST_MNEMONIC } from './utils/constants' +import { fetchTokenBalance } from './utils/currency' +import { fetchBlockNumer } from './utils/chain' +import { fetchAskDepth, fetchBidDepth } from './utils/depth' + +const clients = createProxyClients([11, 12]) +const account = mnemonicToAccount(TEST_MNEMONIC) + +afterEach(async () => { + const blockNumber = await fetchBlockNumer() + await Promise.all( + clients.map(({ testClient }) => { + return testClient.reset({ + jsonRpcUrl: FORK_URL, + blockNumber, + }) + }), + ) +}) + +test('market order in not open market', async () => { + const { publicClient } = clients[0] + expect( + await marketOrder( + cloberTestChain.id, + '0x447ad4a108b5540c220f9f7e83723ac87c0f8fd8', + '0x447ad4a108b5540c220f9f7e83723ac87c0f8fd8', + '0x0000000000000000000000000000000000000000', + '10', + { rpcUrl: publicClient.transport.url! }, + ).catch((e) => e.message), + ).toEqual(` + import { openMarket } from '@clober-dex/v2-sdk' + + const transaction = await openMarket( + 421614, + '0x447ad4a108b5540c220f9f7e83723ac87c0f8fd8', + '0x0000000000000000000000000000000000000000', + ) + `) +}) + +test('market bid', async () => { + const { walletClient, publicClient } = clients[0] + const signature = await signERC20Permit( + cloberTestChain.id, + account, + '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + '1000000', + { rpcUrl: publicClient.transport.url! }, + ) + const transaction = await marketOrder( + cloberTestChain.id, + account.address, + '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + '0x0000000000000000000000000000000000000000', + '1000000', + { signature, rpcUrl: publicClient.transport.url! }, + ) + + const [beforeUSDCBalance, beforeETHBalance, beforeAskDepth] = + await Promise.all([ + fetchTokenBalance( + cloberTestChain.id, + '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + account.address, + publicClient.transport.url!, + ), + publicClient.getBalance({ + address: account.address, + }), + fetchAskDepth(publicClient.transport.url!), + ]) + + await walletClient.sendTransaction({ ...transaction!, account }) + + const [afterUSDCBalance, afterETHBalance, afterAskDepth] = await Promise.all([ + fetchTokenBalance( + cloberTestChain.id, + '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + account.address, + publicClient.transport.url!, + ), + publicClient.getBalance({ + address: account.address, + }), + fetchAskDepth(publicClient.transport.url!), + ]) + + expect(Number(beforeUSDCBalance)).toBeGreaterThan(Number(afterUSDCBalance)) + expect(Number(afterETHBalance)).toBeGreaterThan(Number(beforeETHBalance)) + expect(beforeAskDepth.length).toBeGreaterThan(afterAskDepth.length) + expect(afterAskDepth.length).toBe(0) +}) + +test('market ask', async () => { + const { walletClient, publicClient } = clients[1] + const transaction = await marketOrder( + cloberTestChain.id, + account.address, + '0x0000000000000000000000000000000000000000', + '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + '10', + { rpcUrl: publicClient.transport.url! }, + ) + + const [beforeUSDCBalance, beforeETHBalance, beforeBidDepth] = + await Promise.all([ + fetchTokenBalance( + cloberTestChain.id, + '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + account.address, + publicClient.transport.url!, + ), + publicClient.getBalance({ + address: account.address, + }), + fetchBidDepth(publicClient.transport.url!), + ]) + + await walletClient.sendTransaction({ ...transaction!, account }) + + const [afterUSDCBalance, afterETHBalance, afterBidDepth] = await Promise.all([ + fetchTokenBalance( + cloberTestChain.id, + '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + account.address, + publicClient.transport.url!, + ), + publicClient.getBalance({ + address: account.address, + }), + fetchBidDepth(publicClient.transport.url!), + ]) + + expect(Number(afterUSDCBalance)).toBeGreaterThan(Number(beforeUSDCBalance)) + expect(Number(beforeETHBalance)).toBeGreaterThan(Number(afterETHBalance)) + expect(beforeBidDepth.length).toBeGreaterThan(afterBidDepth.length) + expect(afterBidDepth.length).toBe(0) +})