diff --git a/src/opendex/complete.ts b/src/opendex/complete.ts index 2041949..0775ea2 100644 --- a/src/opendex/complete.ts +++ b/src/opendex/complete.ts @@ -1,6 +1,8 @@ import { BigNumber } from 'bignumber.js'; +import { Exchange } from 'ccxt'; import { Observable } from 'rxjs'; import { exhaustMap } from 'rxjs/operators'; +import { getCentralizedExchangeAssets$ } from '../centralized/assets'; import { Config } from '../config'; import { Loggers } from '../logger'; import { @@ -12,13 +14,10 @@ import { getOpenDEXassets$ } from './assets'; import { logAssetBalance, parseOpenDEXassets } from './assets-utils'; import { CreateOpenDEXordersParams } from './create-orders'; import { tradeInfoToOpenDEXorders } from './orders'; -import { removeOpenDEXorders$ } from './remove-orders'; import { getXudBalance$ } from './xud/balance'; import { getXudClient$ } from './xud/client'; import { createXudOrder$ } from './xud/create-order'; import { getXudTradingLimits$ } from './xud/trading-limits'; -import { getCentralizedExchangeAssets$ } from '../centralized/assets'; -import { Exchange } from 'ccxt'; type GetOpenDEXcompleteParams = { config: Config; @@ -82,7 +81,6 @@ const getOpenDEXcomplete$ = ({ getTradeInfo, getXudClient$, createXudOrder$, - removeOpenDEXorders$, tradeInfoToOpenDEXorders, }); }) diff --git a/src/opendex/create-orders.spec.ts b/src/opendex/create-orders.spec.ts index 37480fa..9795bca 100644 --- a/src/opendex/create-orders.spec.ts +++ b/src/opendex/create-orders.spec.ts @@ -1,3 +1,4 @@ +import { status } from '@grpc/grpc-js'; import { Observable } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { XudClient } from '../proto/xudrpc_grpc_pb'; @@ -5,6 +6,7 @@ import { PlaceOrderResponse } from '../proto/xudrpc_pb'; import { getLoggers, testConfig } from '../test-utils'; import { TradeInfo } from '../trade/info'; import { createOpenDEXorders$, OpenDEXorders } from './create-orders'; +import { TestError } from '../test-utils'; let testScheduler: TestScheduler; const testSchedulerSetup = () => { @@ -16,27 +18,40 @@ const testSchedulerSetup = () => { type CreateOpenDEXordersInputEvents = { xudClient$: string; xudOrder$: string; - removeOpenDEXorders$: string; + replaceXudOrder$: string; }; const assertCreateOpenDEXorders = ( inputEvents: CreateOpenDEXordersInputEvents, - expected: string + expected: string, + inputErrors?: { + xudOrder$?: TestError; + replaceXudOrder$?: TestError; + } ) => { testScheduler.run(helpers => { const { cold, expectObservable } = helpers; const getTradeInfo = (): TradeInfo => { return ('mock trade info' as unknown) as TradeInfo; }; - const createXudOrder$ = () => { - return cold(inputEvents.xudOrder$) as Observable; + const createXudOrder$ = (createOrderParams: any) => { + if (createOrderParams.replaceOrderId) { + return cold( + inputEvents.replaceXudOrder$, + {}, + inputErrors?.replaceXudOrder$ + ) as Observable; + } else { + return cold( + inputEvents.xudOrder$, + {}, + inputErrors?.xudOrder$ + ) as Observable; + } }; const getXudClient$ = () => { return cold(inputEvents.xudClient$) as Observable; }; - const removeOpenDEXorders$ = () => { - return cold(inputEvents.removeOpenDEXorders$) as Observable; - }; const tradeInfoToOpenDEXorders = (v: any) => { return (v as unknown) as OpenDEXorders; }; @@ -45,7 +60,6 @@ const assertCreateOpenDEXorders = ( getXudClient$, createXudOrder$, tradeInfoToOpenDEXorders, - removeOpenDEXorders$, logger: getLoggers().global, config: testConfig(), }); @@ -61,10 +75,36 @@ describe('createOpenDEXorders$', () => { it('creates buy and sell orders', () => { const inputEvents = { xudClient$: '1s a', - removeOpenDEXorders$: '4s a', - xudOrder$: '1s (a|)', + replaceXudOrder$: '1s (a|)', + xudOrder$: '', + }; + const expected = '2s (a|)'; + assertCreateOpenDEXorders(inputEvents, expected); + }); + + it('throws if unknown error for repace order', () => { + const inputEvents = { + xudClient$: '1s a', + replaceXudOrder$: '1s #', + xudOrder$: '', }; - const expected = '6s (a|)'; + const expected = '2s #'; assertCreateOpenDEXorders(inputEvents, expected); }); + + it('retries without replaceOrderId if grpc.NOT_FOUND error', () => { + const inputEvents = { + xudClient$: '1s a', + xudOrder$: '1s (a|)', + replaceXudOrder$: '1s #', + }; + const inputErrors = { + replaceXudOrder$: { + code: status.NOT_FOUND, + message: 'NOT FOUND', + }, + }; + const expected = '3s (a|)'; + assertCreateOpenDEXorders(inputEvents, expected, inputErrors); + }); }); diff --git a/src/opendex/create-orders.ts b/src/opendex/create-orders.ts index 953d24d..102b6d3 100644 --- a/src/opendex/create-orders.ts +++ b/src/opendex/create-orders.ts @@ -1,16 +1,18 @@ -import { forkJoin, Observable } from 'rxjs'; -import { mapTo, mergeMap, take, tap } from 'rxjs/operators'; +import { status } from '@grpc/grpc-js'; +import { forkJoin, Observable, throwError } from 'rxjs'; +import { catchError, mapTo, mergeMap, take, tap } from 'rxjs/operators'; import { Config } from '../config'; import { Logger } from '../logger'; import { XudClient } from '../proto/xudrpc_grpc_pb'; import { PlaceOrderResponse } from '../proto/xudrpc_pb'; import { TradeInfo } from '../trade/info'; -import { OpenDEXorders, TradeInfoToOpenDEXordersParams } from './orders'; -import { processListorders } from './process-listorders'; -import { RemoveOpenDEXordersParams } from './remove-orders'; +import { + OpenDEXorders, + TradeInfoToOpenDEXordersParams, + createOrderID, +} from './orders'; import { CreateXudOrderParams } from './xud/create-order'; -import { listXudOrders$ } from './xud/list-orders'; -import { removeXudOrder$ } from './xud/remove-order'; +import { OrderSide } from '../proto/xudrpc_pb'; type CreateOpenDEXordersParams = { config: Config; @@ -21,13 +23,6 @@ type CreateOpenDEXordersParams = { config, }: TradeInfoToOpenDEXordersParams) => OpenDEXorders; getXudClient$: (config: Config) => Observable; - removeOpenDEXorders$: ({ - config, - getXudClient$, - listXudOrders$, - removeXudOrder$, - processListorders, - }: RemoveOpenDEXordersParams) => Observable; createXudOrder$: ({ client, logger, @@ -45,42 +40,55 @@ const createOpenDEXorders$ = ({ getTradeInfo, tradeInfoToOpenDEXorders, getXudClient$, - removeOpenDEXorders$, createXudOrder$, }: CreateOpenDEXordersParams): Observable => { return getXudClient$(config).pipe( tap(() => logger.trace('Starting to update OpenDEX orders')), - // remove all existing orders - mergeMap(client => { - return removeOpenDEXorders$({ - config, - getXudClient$, - listXudOrders$, - removeXudOrder$, - processListorders, - }).pipe( - tap(() => - logger.trace( - `Removed all open orders for ${config.BASEASSET}/${config.QUOTEASSET}` - ) - ), - mapTo(client) - ); - }), // create new buy and sell orders mergeMap(client => { + // build orders based on all the available trade info const { buyOrder, sellOrder } = tradeInfoToOpenDEXorders({ config, tradeInfo: getTradeInfo(), }); + // try replacing existing buy order const buyOrder$ = createXudOrder$({ ...{ client, logger }, ...buyOrder, - }); + ...{ + replaceOrderId: createOrderID(config, OrderSide.BUY), + }, + }).pipe( + catchError(e => { + if (e.code === status.NOT_FOUND) { + // place order if existing one does not exist + return createXudOrder$({ + ...{ client, logger }, + ...buyOrder, + }); + } + return throwError(e); + }) + ); + // try replacing existing sell order const sellOrder$ = createXudOrder$({ ...{ client, logger }, ...sellOrder, - }); + ...{ + replaceOrderId: createOrderID(config, OrderSide.SELL), + }, + }).pipe( + catchError(e => { + if (e.code === status.NOT_FOUND) { + // place order if existing one does not exist + return createXudOrder$({ + ...{ client, logger }, + ...sellOrder, + }); + } + return throwError(e); + }) + ); const ordersComplete$ = forkJoin(sellOrder$, buyOrder$).pipe(mapTo(true)); return ordersComplete$; }), diff --git a/src/opendex/orders.spec.ts b/src/opendex/orders.spec.ts index 6b3708d..9e5c51f 100644 --- a/src/opendex/orders.spec.ts +++ b/src/opendex/orders.spec.ts @@ -33,7 +33,7 @@ const assertTradeInfoToOpenDEXorders = ({ orderSide: OrderSide.BUY, pairId, price: expected.buyPrice.toNumber(), - orderId: expect.any(String), + orderId: 'arby-ETH/BTC-buy-order', }) ); } @@ -46,7 +46,7 @@ const assertTradeInfoToOpenDEXorders = ({ orderSide: OrderSide.SELL, pairId, price: expected.sellPrice.toNumber(), - orderId: expect.any(String), + orderId: 'arby-ETH/BTC-sell-order', }) ); } diff --git a/src/opendex/orders.ts b/src/opendex/orders.ts index 24fad47..d5e29f9 100644 --- a/src/opendex/orders.ts +++ b/src/opendex/orders.ts @@ -1,5 +1,4 @@ import BigNumber from 'bignumber.js'; -import { v4 as uuidv4 } from 'uuid'; import { Config } from '../config'; import { OrderSide } from '../proto/xudrpc_pb'; import { TradeInfo } from '../trade/info'; @@ -11,6 +10,7 @@ type OpenDEXorder = { pairId: string; price: number; orderId: string; + replaceOrderId?: string; }; type OpenDEXorders = { @@ -23,6 +23,13 @@ type TradeInfoToOpenDEXordersParams = { config: Config; }; +const createOrderID = (config: Config, orderSide: OrderSide): string => { + const pairId = `${config.BASEASSET}/${config.QUOTEASSET}`; + return orderSide === OrderSide.BUY + ? `arby-${pairId}-buy-order` + : `arby-${pairId}-sell-order`; +}; + const tradeInfoToOpenDEXorders = ({ tradeInfo, config, @@ -59,14 +66,14 @@ const tradeInfoToOpenDEXorders = ({ orderSide: OrderSide.BUY, pairId, price: buyPrice.toNumber(), - orderId: uuidv4(), + orderId: createOrderID(config, OrderSide.BUY), }; const sellOrder = { quantity: coinsToSats(new BigNumber(sellQuantity.toFixed(8, 1)).toNumber()), orderSide: OrderSide.SELL, pairId, price: sellPrice.toNumber(), - orderId: uuidv4(), + orderId: createOrderID(config, OrderSide.SELL), }; return { buyOrder, @@ -78,5 +85,6 @@ export { OpenDEXorders, OpenDEXorder, tradeInfoToOpenDEXorders, + createOrderID, TradeInfoToOpenDEXordersParams, }; diff --git a/src/opendex/xud/create-order.spec.ts b/src/opendex/xud/create-order.spec.ts index 9075830..4aa75be 100644 --- a/src/opendex/xud/create-order.spec.ts +++ b/src/opendex/xud/create-order.spec.ts @@ -2,11 +2,12 @@ import { XudClient } from '../../proto/xudrpc_grpc_pb'; import { OrderSide, PlaceOrderRequest } from '../../proto/xudrpc_pb'; import { createXudOrder$ } from './create-order'; import { getLoggers } from '../../test-utils'; +import { OpenDEXorder } from '../orders'; jest.mock('../../proto/xudrpc_grpc_pb'); jest.mock('../../proto/xudrpc_pb'); -const getTestXudOrderParams = () => { +const getTestXudOrderParams = (): OpenDEXorder => { return { quantity: 123, orderSide: OrderSide.BUY, @@ -22,10 +23,10 @@ describe('createXudOrder$', () => { }); test('success', done => { - expect.assertions(7); + expect.assertions(8); const expectedResponse = 'expectedResponse'; const client = ({ - placeOrderSync: (req: any, cb: any) => { + placeOrderSync: (_req: any, cb: any) => { cb(null, expectedResponse); }, } as unknown) as XudClient; @@ -53,6 +54,52 @@ describe('createXudOrder$', () => { expect(PlaceOrderRequest.prototype.setOrderId).toHaveBeenCalledWith( order.orderId ); + expect( + PlaceOrderRequest.prototype.setReplaceOrderId + ).not.toHaveBeenCalled(); + }, + complete: done, + }); + }); + + test('success replace order', done => { + expect.assertions(8); + const expectedResponse = 'expectedResponse'; + const client = ({ + placeOrderSync: (_req: any, cb: any) => { + cb(null, expectedResponse); + }, + } as unknown) as XudClient; + const order = { + ...getTestXudOrderParams(), + ...{ replaceOrderId: '123abc' }, + }; + const createOrder$ = createXudOrder$({ + ...{ client, logger: getLoggers().opendex }, + ...order, + }); + createOrder$.subscribe({ + next: actualResponse => { + expect(actualResponse).toEqual(expectedResponse); + expect(PlaceOrderRequest).toHaveBeenCalledTimes(1); + expect(PlaceOrderRequest.prototype.setQuantity).toHaveBeenCalledWith( + order.quantity + ); + expect(PlaceOrderRequest.prototype.setSide).toHaveBeenCalledWith( + order.orderSide + ); + expect(PlaceOrderRequest.prototype.setPairId).toHaveBeenCalledWith( + order.pairId + ); + expect(PlaceOrderRequest.prototype.setPrice).toHaveBeenCalledWith( + order.price + ); + expect(PlaceOrderRequest.prototype.setOrderId).toHaveBeenCalledWith( + order.orderId + ); + expect( + PlaceOrderRequest.prototype.setReplaceOrderId + ).toHaveBeenCalledWith(order.replaceOrderId); }, complete: done, }); diff --git a/src/opendex/xud/create-order.ts b/src/opendex/xud/create-order.ts index 31dad6f..27e0eb6 100644 --- a/src/opendex/xud/create-order.ts +++ b/src/opendex/xud/create-order.ts @@ -27,10 +27,12 @@ const createXudOrder$ = ({ pairId, price, orderId, + replaceOrderId, }: CreateXudOrderParams): Observable => { if (quantity > 0) { + const CREATING_OR_REPLACING = replaceOrderId ? 'Replacing' : 'Creating'; logger.trace( - `Creating ${pairId} ${ + `${CREATING_OR_REPLACING} ${pairId} ${ orderSideMapping[orderSide] } order with id ${orderId}, quantity ${satsToCoinsStr( quantity @@ -42,6 +44,9 @@ const createXudOrder$ = ({ request.setPairId(pairId); request.setPrice(price); request.setOrderId(orderId); + if (replaceOrderId) { + request.setReplaceOrderId(replaceOrderId); + } const createXudOrder$ = new Observable(subscriber => { client.placeOrderSync( request,