diff --git a/.changeset/chatty-maps-do.md b/.changeset/chatty-maps-do.md new file mode 100644 index 0000000..c7f5ded --- /dev/null +++ b/.changeset/chatty-maps-do.md @@ -0,0 +1,5 @@ +--- +"@mangrovedao/mgv": patch +--- + +Add result from logs on update, and remove diff --git a/src/actions/order/remove.test.ts b/src/actions/order/remove.test.ts new file mode 100644 index 0000000..f315e32 --- /dev/null +++ b/src/actions/order/remove.test.ts @@ -0,0 +1,113 @@ +import { parseUnits } from 'viem' +import { describe, expect, inject, it } from 'vitest' +import { BS, Order } from '~mgv/lib/enums.js' +import { + limitOrderResultFromLogs, + removeOrderResultFromLogs, +} from '~mgv/lib/limit-order.js' +import { getClient } from '~test/src/client.js' +import { getBook } from '../book.js' +import { simulateLimitOrder } from './new.js' +import { simulateRemoveOrder } from './remove.js' + +const client = getClient() +const { wethUSDC } = inject('markets') +const params = inject('mangrove') + +describe('remove order', () => { + it('removes the order: no deprovisionning', async () => { + // create an order + const book = await getBook(client, params, wethUSDC) + + const baseAmount = parseUnits('1', wethUSDC.base.decimals) + const quoteAmount = parseUnits('3000', wethUSDC.quote.decimals) + + const { request } = await simulateLimitOrder(client, params, wethUSDC, { + baseAmount, + quoteAmount, + restingOrderGasreq: 250_000n, + bs: BS.buy, + book, + orderType: Order.PO, + }) + const tx = await client.writeContract(request) + const receipt = await client.waitForTransactionReceipt({ hash: tx }) + const result = limitOrderResultFromLogs(params, wethUSDC, { + logs: receipt.logs, + user: client.account.address, + bs: BS.buy, + }) + expect(result.offer).toBeDefined() + expect(result.offer!.id).toBe(1n) + + const { request: removeRequest } = await simulateRemoveOrder( + client, + params, + wethUSDC, + { + bs: BS.buy, + offerId: 1n, + deprovision: false, + }, + ) + const removeTx = await client.writeContract(removeRequest) + const removeReceipt = await client.waitForTransactionReceipt({ + hash: removeTx, + }) + const removeResult = removeOrderResultFromLogs(params, wethUSDC, { + logs: removeReceipt.logs, + offerId: 1n, + bs: BS.buy, + }) + expect(removeResult.success).toBeTruthy() + expect(removeResult.deprovision).toBeFalsy() + }) + + it('removes the order: deprovisionning', async () => { + // create an order + const book = await getBook(client, params, wethUSDC) + + const baseAmount = parseUnits('1', wethUSDC.base.decimals) + const quoteAmount = parseUnits('3000', wethUSDC.quote.decimals) + + const { request } = await simulateLimitOrder(client, params, wethUSDC, { + baseAmount, + quoteAmount, + restingOrderGasreq: 250_000n, + bs: BS.buy, + book, + orderType: Order.PO, + }) + const tx = await client.writeContract(request) + const receipt = await client.waitForTransactionReceipt({ hash: tx }) + const result = limitOrderResultFromLogs(params, wethUSDC, { + logs: receipt.logs, + user: client.account.address, + bs: BS.buy, + }) + expect(result.offer).toBeDefined() + expect(result.offer!.id).toBe(1n) + + const { request: removeRequest } = await simulateRemoveOrder( + client, + params, + wethUSDC, + { + bs: BS.buy, + offerId: 1n, + deprovision: true, + }, + ) + const removeTx = await client.writeContract(removeRequest) + const removeReceipt = await client.waitForTransactionReceipt({ + hash: removeTx, + }) + const removeResult = removeOrderResultFromLogs(params, wethUSDC, { + logs: removeReceipt.logs, + offerId: 1n, + bs: BS.buy, + }) + expect(removeResult.success).toBeTruthy() + expect(removeResult.deprovision).toBeTruthy() + }) +}) diff --git a/src/actions/order/update.test.ts b/src/actions/order/update.test.ts new file mode 100644 index 0000000..e6f2aa0 --- /dev/null +++ b/src/actions/order/update.test.ts @@ -0,0 +1,139 @@ +import { parseUnits } from 'viem' +import { describe, expect, inject, it } from 'vitest' +import { BS, Order } from '~mgv/lib/enums.js' +import { + limitOrderResultFromLogs, + setExpirationResultFromLogs, + updateOrderResultFromLogs, +} from '~mgv/lib/limit-order.js' +import { tickFromVolumes } from '~mgv/lib/tick.js' +import { getClient } from '~test/src/client.js' +import { getBook } from '../book.js' +import { simulateLimitOrder } from './new.js' +import { simulateSetExpiration, simulateUpdateOrder } from './update.js' + +const client = getClient() +const { wethUSDC } = inject('markets') +const params = inject('mangrove') + +describe('update order', () => { + it('updates an order', async () => { + // create an order + const book = await getBook(client, params, wethUSDC) + + let baseAmount = parseUnits('1', wethUSDC.base.decimals) + let quoteAmount = parseUnits('3000', wethUSDC.quote.decimals) + + const { request } = await simulateLimitOrder(client, params, wethUSDC, { + baseAmount, + quoteAmount, + restingOrderGasreq: 250_000n, + bs: BS.buy, + book, + orderType: Order.PO, + }) + const tx = await client.writeContract(request) + const receipt = await client.waitForTransactionReceipt({ hash: tx }) + const result = limitOrderResultFromLogs(params, wethUSDC, { + logs: receipt.logs, + user: client.account.address, + bs: BS.buy, + }) + expect(result.offer).toBeDefined() + expect(result.offer!.id).toBe(1n) + expect(result.offer!.gives).toApproximateEqual(quoteAmount) + expect(result.offer!.wants).toApproximateEqual(baseAmount) + expect(result.offer!.tick).toBe( + -tickFromVolumes(quoteAmount, baseAmount, wethUSDC.tickSpacing), + ) + + baseAmount *= 2n + quoteAmount *= 3n + + const { request: updateRequest } = await simulateUpdateOrder( + client, + params, + wethUSDC, + { + baseAmount, + quoteAmount, + restingOrderGasreq: 250_000n, + bs: BS.buy, + book, + offerId: 1n, + }, + ) + const updateTx = await client.writeContract(updateRequest) + const updateReceipt = await client.waitForTransactionReceipt({ + hash: updateTx, + }) + const updateResult = updateOrderResultFromLogs(params, wethUSDC, { + logs: updateReceipt.logs, + bs: BS.buy, + offerId: 1n, + }) + expect(updateResult.gives).toApproximateEqual(quoteAmount) + expect(updateResult.wants).toApproximateEqual(baseAmount) + expect(updateResult.tick).toBe( + tickFromVolumes(baseAmount, quoteAmount, wethUSDC.tickSpacing), + ) + expect(updateResult.gasreq).toBe(250_000n) + }) + + it('updates an order: expiry', async () => { + // create an order + const book = await getBook(client, params, wethUSDC) + + let baseAmount = parseUnits('1', wethUSDC.base.decimals) + let quoteAmount = parseUnits('3000', wethUSDC.quote.decimals) + + let expiry = BigInt(Math.floor(Date.now() / 1000) + 60 * 60 * 24) + + const { request } = await simulateLimitOrder(client, params, wethUSDC, { + baseAmount, + quoteAmount, + restingOrderGasreq: 250_000n, + bs: BS.buy, + book, + orderType: Order.PO, + expiryDate: expiry, + }) + const tx = await client.writeContract(request) + const receipt = await client.waitForTransactionReceipt({ hash: tx }) + const result = limitOrderResultFromLogs(params, wethUSDC, { + logs: receipt.logs, + user: client.account.address, + bs: BS.buy, + }) + expect(result.offer).toBeDefined() + expect(result.offer!.id).toBe(1n) + expect(result.offer!.expiry).toBe(expiry) + + baseAmount *= 2n + quoteAmount *= 3n + + expiry += 60n * 60n * 24n + + const { request: updateRequest } = await simulateSetExpiration( + client, + params, + wethUSDC, + { + bs: BS.buy, + offerId: 1n, + expiryDate: expiry, + }, + ) + const updateTx = await client.writeContract(updateRequest) + const updateReceipt = await client.waitForTransactionReceipt({ + hash: updateTx, + }) + const updateResult = setExpirationResultFromLogs(params, wethUSDC, { + logs: updateReceipt.logs, + bs: BS.buy, + offerId: 1n, + }) + + expect(updateResult).toBe(expiry) + }) +}) diff --git a/src/index.ts b/src/index.ts index f3dabae..c14d3b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,12 @@ export type { MarketOrderSimulationParams, MarketOrderResultFromLogsParams, + RawLimitOrderResultFromLogsParams, LimitOrderResultFromLogsParams, LimitOrderResult, + GetDefaultLimitOrderGasreqParams, + RawUpdateOrderResultFromLogsParams, + UpdateOrderResultFromLogsParams, AmountsToHumanPriceParams, AmountsParams, AmountsOutput, @@ -17,6 +21,11 @@ export type { RawKandelParams, KandelParams, ValidateParamsResult, + RawSetExpirationResultFromLogsParams, + SetExpirationResultFromLogsParams, + RawRemoveOrderResultFromLogsParams, + RemoveOrderResult, + RemoveOrderResultFromLogsParams, } from './lib/index.js' export { @@ -41,11 +50,20 @@ export { marketOrderSimulation, marketOrderResultFromLogs, limitOrderResultFromLogs, + orderLabel, + rawUpdateOrderResultFromLogs, + updateOrderResultFromLogs, + ParseUpdateOrderLogsError, CreateDistributionError, createGeometricDistribution, seederEventsABI, getKandelsFromLogs, validateKandelParams, + getDefaultLimitOrderGasreq, + rawSetExpirationResultFromLogs, + setExpirationResultFromLogs, + rawRemoveOrderResultFromLogs, + removeOrderResultFromLogs, } from './lib/index.js' // --- Types --- diff --git a/src/lib/index.ts b/src/lib/index.ts index f4ba039..2c267fb 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -60,6 +60,13 @@ export type { LimitOrderResultFromLogsParams, LimitOrderResult, GetDefaultLimitOrderGasreqParams, + RawUpdateOrderResultFromLogsParams, + UpdateOrderResultFromLogsParams, + RawSetExpirationResultFromLogsParams, + SetExpirationResultFromLogsParams, + RawRemoveOrderResultFromLogsParams, + RemoveOrderResult, + RemoveOrderResultFromLogsParams, } from './limit-order.js' export { @@ -67,6 +74,13 @@ export { limitOrderResultFromLogs, getDefaultLimitOrderGasreq, orderLabel, + rawUpdateOrderResultFromLogs, + updateOrderResultFromLogs, + ParseUpdateOrderLogsError, + rawSetExpirationResultFromLogs, + setExpirationResultFromLogs, + rawRemoveOrderResultFromLogs, + removeOrderResultFromLogs, } from './limit-order.js' // local diff --git a/src/lib/limit-order.ts b/src/lib/limit-order.ts index 7ac9ba7..bd95802 100644 --- a/src/lib/limit-order.ts +++ b/src/lib/limit-order.ts @@ -227,3 +227,202 @@ export function orderLabel( ): (typeof _orderLabel)[TOrder] { return _orderLabel[order] } + +export type RawUpdateOrderResultFromLogsParams = { + logs: Log[] + mgv: Address + mgvOrder: Address + olKey: OLKey + offerId: bigint +} + +export type UpdateOrderResult = { + tick: bigint + gives: bigint + wants: bigint + gasprice: bigint + gasreq: bigint +} + +export class ParseUpdateOrderLogsError extends Error { + constructor(message: string) { + super(message) + this.name = 'ParseUpdateOrderLogsError' + } +} + +export function rawUpdateOrderResultFromLogs( + params: RawUpdateOrderResultFromLogsParams, +): UpdateOrderResult { + const events = parseEventLogs({ + abi: mgvEventsABI, + eventName: 'OfferWrite', + logs: params.logs.filter((log) => { + return isAddressEqual(log.address, params.mgv) + }), + }) + + const writeEvent = events.findLast((e) => { + return ( + e.args.olKeyHash.toLowerCase() === hash(params.olKey).toLowerCase() && + isAddressEqual(e.args.maker, params.mgvOrder) + ) + }) + + if (!writeEvent) + throw new ParseUpdateOrderLogsError('OfferWrite event not found') + + return { + tick: writeEvent.args.tick, + gives: writeEvent.args.gives, + wants: inboundFromOutbound(writeEvent.args.tick, writeEvent.args.gives), + gasprice: writeEvent.args.gasprice, + gasreq: writeEvent.args.gasreq, + } +} + +export type UpdateOrderResultFromLogsParams = { + logs: Log[] + offerId: bigint + bs: BS +} + +export function updateOrderResultFromLogs( + actionParams: MangroveActionsDefaultParams, + market: MarketParams, + params: UpdateOrderResultFromLogsParams, +): UpdateOrderResult { + const { + base: { address: base }, + quote: { address: quote }, + tickSpacing, + } = market + // if we buy, the resulting order has outbound as quote and inbound as base + // if we sell, the resulting order has outbound as base and inbound as quote + const olKey: OLKey = + params.bs === BS.buy + ? { outbound_tkn: quote, inbound_tkn: base, tickSpacing } + : { outbound_tkn: base, inbound_tkn: quote, tickSpacing } + + return rawUpdateOrderResultFromLogs({ + ...params, + ...actionParams, + olKey, + }) +} + +export type RawSetExpirationResultFromLogsParams = { + logs: Log[] + olKey: OLKey + offerId: bigint + mgvOrder: Address +} + +export function rawSetExpirationResultFromLogs( + params: RawSetExpirationResultFromLogsParams, +): bigint | undefined { + const events = parseEventLogs({ + abi: mgvOrderEventsABI, + eventName: 'SetReneging', + logs: params.logs.filter((log) => { + return isAddressEqual(log.address, params.mgvOrder) + }), + }) + + const expiryEvent = events.findLast((e) => { + return ( + e.args.offerId === params.offerId && + e.args.olKeyHash.toLowerCase() === hash(params.olKey).toLowerCase() + ) + }) + + return expiryEvent?.args.date +} + +export type SetExpirationResultFromLogsParams = { + logs: Log[] + offerId: bigint + bs: BS +} + +export function setExpirationResultFromLogs( + actionsParams: MangroveActionsDefaultParams, + market: MarketParams, + params: SetExpirationResultFromLogsParams, +): bigint | undefined { + const { + base: { address: base }, + quote: { address: quote }, + tickSpacing, + } = market + const olKey: OLKey = + params.bs === BS.buy + ? { outbound_tkn: quote, inbound_tkn: base, tickSpacing } + : { outbound_tkn: base, inbound_tkn: quote, tickSpacing } + return rawSetExpirationResultFromLogs({ + ...params, + ...actionsParams, + olKey, + }) +} + +export type RawRemoveOrderResultFromLogsParams = { + logs: Log[] + mgv: Address + olKey: OLKey + offerId: bigint +} + +export type RemoveOrderResult = { + success: boolean + deprovision: boolean +} + +export function rawRemoveOrderResultFromLogs( + params: RawRemoveOrderResultFromLogsParams, +): RemoveOrderResult { + const events = parseEventLogs({ + abi: mgvEventsABI, + eventName: 'OfferRetract', + logs: params.logs.filter((log) => { + return isAddressEqual(log.address, params.mgv) + }), + }) + const event = events.findLast((e) => { + return ( + e.args.olKeyHash.toLowerCase() === hash(params.olKey).toLowerCase() && + e.args.id === params.offerId + ) + }) + return { + success: !!event, + deprovision: event?.args.deprovision ?? false, + } +} + +export type RemoveOrderResultFromLogsParams = { + logs: Log[] + offerId: bigint + bs: BS +} + +export function removeOrderResultFromLogs( + actionParams: MangroveActionsDefaultParams, + market: MarketParams, + params: RemoveOrderResultFromLogsParams, +): RemoveOrderResult { + const { + base: { address: base }, + quote: { address: quote }, + tickSpacing, + } = market + const olKey: OLKey = + params.bs === BS.buy + ? { outbound_tkn: quote, inbound_tkn: base, tickSpacing } + : { outbound_tkn: base, inbound_tkn: quote, tickSpacing } + return rawRemoveOrderResultFromLogs({ + ...params, + ...actionParams, + olKey, + }) +} diff --git a/test/setup.ts b/test/setup.ts index a890689..7566e54 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,5 +1,6 @@ import { type Address, isAddress, isAddressEqual } from 'viem' -import { expect } from 'vitest' +import { afterEach, expect } from 'vitest' +import { getClient } from './src/client.js' expect.extend({ toApproximateEqual: ( @@ -40,3 +41,8 @@ expect.extend({ } }, }) + +afterEach(async () => { + const client = getClient() + await client.reset() +}) diff --git a/test/src/client.ts b/test/src/client.ts index 2bfe3cc..a60bcdd 100644 --- a/test/src/client.ts +++ b/test/src/client.ts @@ -5,6 +5,7 @@ import { publicActions, walletActions, } from 'viem' +// import { ipc } from 'viem/node' import { privateKeyToAccount } from 'viem/accounts' import { foundry } from 'viem/chains' import { multicall } from '~test/globalSetup.js' @@ -43,11 +44,15 @@ export function getClient() { }, }) + const httpTransport = http( + `http://localhost:${process.env.PROXY_PORT || 8545}/${poolId}`, + ) + + // const ipcTransport = ipc(`/tmp/anvil-${poolId}.ipc`) + return createTestClient({ chain: mgvTestChain, - transport: http( - `http://localhost:${process.env.PROXY_PORT || 8545}/${poolId}`, - ), + transport: httpTransport, mode: 'anvil', account: privateKeyToAccount(accounts[0].privateKey), })