From 60ea756cd30a97b6e7b78de1e992757d313d693b Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Tue, 29 Aug 2023 16:54:23 +0530 Subject: [PATCH 01/11] add getResponseState operation --- .../js/src/plugins/rfqModule/RfqClient.ts | 11 + .../rfqModule/operations/getResponseState.ts | 184 +++++++++++ .../src/plugins/rfqModule/operations/index.ts | 1 + packages/js/src/plugins/rfqModule/plugin.ts | 3 + packages/js/tests/unit/responseState.spec.ts | 287 ++++++++++++++++++ 5 files changed, 486 insertions(+) create mode 100644 packages/js/src/plugins/rfqModule/operations/getResponseState.ts create mode 100644 packages/js/tests/unit/responseState.spec.ts diff --git a/packages/js/src/plugins/rfqModule/RfqClient.ts b/packages/js/src/plugins/rfqModule/RfqClient.ts index 1205569cd..932c88267 100644 --- a/packages/js/src/plugins/rfqModule/RfqClient.ts +++ b/packages/js/src/plugins/rfqModule/RfqClient.ts @@ -79,6 +79,9 @@ import { GetSettlementResultInput, getSettlementResultOperation, getSettlementResultHandler, + GetResponseStateInput, + getResponseStateOperation, + getResponseStateHandler, } from './operations'; import { Response } from './models/Response'; @@ -488,4 +491,12 @@ export class RfqClient { this.convergence ); } + + /** {@inheritDoc getResponseState} */ + getResponseState(input: GetResponseStateInput) { + return getResponseStateHandler.handle( + getResponseStateOperation(input), + this.convergence + ); + } } diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseState.ts b/packages/js/src/plugins/rfqModule/operations/getResponseState.ts new file mode 100644 index 000000000..933f109d9 --- /dev/null +++ b/packages/js/src/plugins/rfqModule/operations/getResponseState.ts @@ -0,0 +1,184 @@ +import { Rfq, Response } from '../models'; +import { Operation, SyncOperationHandler, useOperation } from '../../../types'; +const Key = 'GetResponseState' as const; +export type ResponseState = + | 'Kill' + | 'Reclaim' + | 'Cleanup' + | 'Rejected' + | 'Defaulted' + | 'Settle' + | 'Settled' + | 'Expired' + | 'Approve' + | 'Cancelled' + | 'None'; +/** + * getResponseState. + * + * ```ts + * const result = await convergence.rfqs().getResponseState({ + * response, + * rfq, + * }) + * ``` + * + * @group Operations + * @category Constructors + */ +export const getResponseStateOperation = useOperation(Key); + +/** + * @group Operations + * @category Types + */ +export type GetResponseState = Operation< + typeof Key, + GetResponseStateInput, + GetResponseStateOutput +>; + +/** + * @group Operations + * @category Inputs + */ +export type GetResponseStateInput = { + response: Response; + rfq: Rfq; + caller: 'taker' | 'maker'; + responseSide: 'ask' | 'bid'; +}; + +/** + * @group Operations + * @category Outputs + */ +export type GetResponseStateOutput = { + responseState: ResponseState; +}; + +/** + * @group Operations + * @category Handlers + */ +export const getResponseStateHandler: SyncOperationHandler = { + handle: (operation: GetResponseState): GetResponseStateOutput => { + const { response, caller, rfq, responseSide } = operation.input; + if (!response.rfq.equals(rfq.address)) { + throw new Error('Response does not match RFQ'); + } + let state: ResponseState; + const responseState = response.state; + const timestampStart = new Date(Number(rfq.creationTimestamp)); + const timestampExpiry = new Date( + timestampStart.getTime() + Number(rfq.activeWindow) * 1000 + ); + const timestampSettlement = new Date( + timestampExpiry.getTime() + Number(rfq.settlingWindow) * 1000 + ); + const rfqExpired = timestampExpiry.getTime() <= Date.now(); + const settlememtWindowElapsed = timestampSettlement.getTime() <= Date.now(); + const confirmedResponseSide = + (responseSide === 'ask' && response?.confirmed?.side === 'ask') || + (responseSide === 'bid' && response?.confirmed?.side === 'bid'); + + const confirmedInverseResponseSide = + (responseSide === 'ask' && response?.confirmed?.side !== 'ask') || + (responseSide === 'bid' && response?.confirmed?.side !== 'bid'); + + const takerPreparedLegsComplete = + response.takerPreparedLegs === rfq.legs.length; + const makerPreparedLegsComplete = + response.makerPreparedLegs === rfq.legs.length; + const partyPreparedLegsComplete = + caller === 'maker' + ? makerPreparedLegsComplete + : takerPreparedLegsComplete; + const counterpartyPreparedLegsComplete = + caller === 'maker' + ? takerPreparedLegsComplete + : makerPreparedLegsComplete; + + const defaulted = response.defaultingParty + ? response.defaultingParty !== null + : settlememtWindowElapsed && + (!partyPreparedLegsComplete || !counterpartyPreparedLegsComplete); + + const responseConfirmed = response?.confirmed !== null; + switch (caller) { + case 'maker': + switch (true) { + case responseState === 'active' && !responseConfirmed && !rfqExpired: + state = 'Kill'; + break; + case ((responseState === 'active' && + !responseConfirmed && + rfqExpired) || + responseState === 'canceled') && + response.makerCollateralLocked > 0: + state = 'Reclaim'; + break; + case (responseState === 'active' || responseState === 'canceled') && + response.makerCollateralLocked === 0: + state = 'Cleanup'; + break; + case responseConfirmed && confirmedInverseResponseSide: + state = 'Rejected'; + break; + case confirmedResponseSide && + (responseState === 'settling-preparations' || + responseState === 'ready-for-settling') && + !defaulted: + state = 'Settle'; + break; + case defaulted || responseState === 'defaulted': + state = 'Defaulted'; + break; + case !defaulted && responseState === 'settled': + state = 'Settled'; + break; + default: + state = 'None'; + break; + } + break; + + case 'taker': + switch (true) { + case responseState === 'active' && rfqExpired && !responseConfirmed: + state = 'Expired'; + break; + case responseState === 'canceled': + state = 'Cancelled'; + break; + case responseConfirmed && confirmedInverseResponseSide: + state = 'Rejected'; + break; + case responseState === 'active' && !rfqExpired && !responseConfirmed: + state = 'Approve'; + break; + case confirmedResponseSide && + (responseState === 'settling-preparations' || + responseState === 'ready-for-settling') && + !defaulted: + state = 'Settle'; + break; + case defaulted || responseState === 'defaulted': + state = 'Defaulted'; + break; + case responseState === 'settled': + state = 'Settled'; + break; + default: + state = 'None'; + break; + } + break; + default: + state = 'None'; + break; + } + + return { responseState: state }; + }, +}; diff --git a/packages/js/src/plugins/rfqModule/operations/index.ts b/packages/js/src/plugins/rfqModule/operations/index.ts index 018dfd5a4..d03203391 100644 --- a/packages/js/src/plugins/rfqModule/operations/index.ts +++ b/packages/js/src/plugins/rfqModule/operations/index.ts @@ -35,3 +35,4 @@ export * from './unlockResponseCollateral'; export * from './unlockResponsesCollateral'; export * from './unlockRfqsCollateral'; export * from './getSettlementResult'; +export * from './getResponseState'; diff --git a/packages/js/src/plugins/rfqModule/plugin.ts b/packages/js/src/plugins/rfqModule/plugin.ts index a77e84727..286036205 100644 --- a/packages/js/src/plugins/rfqModule/plugin.ts +++ b/packages/js/src/plugins/rfqModule/plugin.ts @@ -73,6 +73,8 @@ import { cleanUpRfqsOperationHandler, getSettlementResultOperation, getSettlementResultHandler, + getResponseStateOperation, + getResponseStateHandler, } from './operations'; import { rfqProgram } from './program'; @@ -182,6 +184,7 @@ export const rfqModule = (): ConvergencePlugin => ({ unlockRfqsCollateralOperationHandler ); op.register(getSettlementResultOperation, getSettlementResultHandler); + op.register(getResponseStateOperation, getResponseStateHandler); convergence.rfqs = function () { return new RfqClient(this); diff --git a/packages/js/tests/unit/responseState.spec.ts b/packages/js/tests/unit/responseState.spec.ts new file mode 100644 index 000000000..cea5ccdb8 --- /dev/null +++ b/packages/js/tests/unit/responseState.spec.ts @@ -0,0 +1,287 @@ +import { expect } from 'expect'; +import { Response } from '../../src/plugins/rfqModule/models/Response'; +import { + Mint, + SpotLegInstrument, + SpotQuoteInstrument, + FixedSize, +} from '../../src'; +import { createUserCvg, sleep } from '../helpers'; +import { BASE_MINT_BTC_PK, QUOTE_MINT_PK } from '../constants'; + +describe('unit.getResponseState', () => { + const takerCvg = createUserCvg('taker'); + const makerCvg = createUserCvg('maker'); + + let baseMintBTC: Mint; + let quoteMint: Mint; + + before(async () => { + baseMintBTC = await takerCvg + .tokens() + .findMintByAddress({ address: BASE_MINT_BTC_PK }); + quoteMint = await takerCvg + .tokens() + .findMintByAddress({ address: QUOTE_MINT_PK }); + }); + + it('[Kill, Reclaim, Cleanup, Cancelled]', async () => { + let refreshedResponse: Response; + const fixedSize: FixedSize = { + type: 'fixed-base', + amount: 19.653_038_331, + }; + const { rfq } = await takerCvg.rfqs().createAndFinalize({ + instruments: [ + await SpotLegInstrument.create(takerCvg, baseMintBTC, 5.12, 'long'), + ], + orderType: 'sell', + quoteAsset: await SpotQuoteInstrument.create(takerCvg, quoteMint), + fixedSize, + }); + const { rfqResponse } = await makerCvg.rfqs().respond({ + rfq: rfq.address, + bid: { + price: 12000, + }, + }); + + //Kill for maker + expect( + takerCvg.rfqs().getResponseState({ + response: rfqResponse, + rfq, + caller: 'maker', + responseSide: 'bid', + }).responseState + ).toBe('Kill'); + + await makerCvg.rfqs().cancelResponse({ response: rfqResponse.address }); + refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ + address: rfqResponse.address, + }); + + //Cancelled for taker + expect( + takerCvg.rfqs().getResponseState({ + response: refreshedResponse, + rfq, + caller: 'taker', + responseSide: 'bid', + }).responseState + ).toBe('Cancelled'); + + //Unlock Response Collateral + refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ + address: rfqResponse.address, + }); + expect( + takerCvg.rfqs().getResponseState({ + response: refreshedResponse, + rfq, + caller: 'maker', + responseSide: 'bid', + }).responseState + ).toBe('Reclaim'); + await makerCvg.rfqs().unlockResponseCollateral({ + response: refreshedResponse.address, + }); + + //Cleanup for maker + refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ + address: rfqResponse.address, + }); + expect( + takerCvg.rfqs().getResponseState({ + response: refreshedResponse, + rfq, + caller: 'maker', + responseSide: 'bid', + }).responseState + ).toBe('Cleanup'); + + await makerCvg.rfqs().cleanUpResponse({ + response: rfqResponse.address, + maker: makerCvg.identity().publicKey, + }); + }); + + it('[Approve, Settle, Settled]', async () => { + let refreshedResponse: Response; + const fixedSize: FixedSize = { + type: 'fixed-base', + amount: 19.653_038_331, + }; + const { rfq } = await takerCvg.rfqs().createAndFinalize({ + instruments: [ + await SpotLegInstrument.create(takerCvg, baseMintBTC, 5.12, 'long'), + ], + orderType: 'sell', + quoteAsset: await SpotQuoteInstrument.create(takerCvg, quoteMint), + fixedSize, + }); + const { rfqResponse } = await makerCvg.rfqs().respond({ + rfq: rfq.address, + bid: { + price: 12000, + }, + }); + + //Approve + expect( + takerCvg.rfqs().getResponseState({ + response: rfqResponse, + rfq, + caller: 'taker', + responseSide: 'bid', + }).responseState + ).toBe('Approve'); + + await takerCvg.rfqs().confirmResponse({ + response: rfqResponse.address, + side: 'bid', + rfq: rfq.address, + }); + + //Settle for maker + refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ + address: rfqResponse.address, + }); + expect( + takerCvg.rfqs().getResponseState({ + response: refreshedResponse, + rfq, + caller: 'maker', + responseSide: 'bid', + }).responseState + ).toBe('Settle'); + + //Settle for taker + expect( + takerCvg.rfqs().getResponseState({ + response: refreshedResponse, + rfq, + caller: 'taker', + responseSide: 'bid', + }).responseState + ).toBe('Settle'); + + await takerCvg.rfqs().prepareSettlement({ + response: rfqResponse.address, + rfq: rfq.address, + legAmountToPrepare: rfq.legs.length, + }); + + await makerCvg.rfqs().prepareSettlement({ + response: rfqResponse.address, + rfq: rfq.address, + legAmountToPrepare: rfq.legs.length, + }); + + await takerCvg.rfqs().settle({ + response: rfqResponse.address, + rfq: rfq.address, + maker: makerCvg.identity().publicKey, + taker: takerCvg.identity().publicKey, + }); + + //Settled + refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ + address: rfqResponse.address, + }); + expect( + takerCvg.rfqs().getResponseState({ + response: refreshedResponse, + rfq, + caller: 'maker', + responseSide: 'bid', + }).responseState + ).toBe('Settled'); + }); + + it('[Expired]', async () => { + const fixedSize: FixedSize = { + type: 'fixed-base', + amount: 19.653_038_331, + }; + const { rfq } = await takerCvg.rfqs().createAndFinalize({ + instruments: [ + await SpotLegInstrument.create(takerCvg, baseMintBTC, 5.12, 'long'), + ], + orderType: 'sell', + quoteAsset: await SpotQuoteInstrument.create(takerCvg, quoteMint), + fixedSize, + activeWindow: 2, + settlingWindow: 1, + }); + const { rfqResponse } = await makerCvg.rfqs().respond({ + rfq: rfq.address, + bid: { + price: 12000, + }, + }); + await sleep(3); + //Expired + expect( + takerCvg.rfqs().getResponseState({ + response: rfqResponse, + rfq, + caller: 'taker', + responseSide: 'bid', + }).responseState + ).toBe('Expired'); + }); + + it('[Rejected, Defaulted]', async () => { + const fixedSize: FixedSize = { + type: 'fixed-base', + amount: 19.653_038_331, + }; + const { rfq } = await takerCvg.rfqs().createAndFinalize({ + instruments: [ + await SpotLegInstrument.create(takerCvg, baseMintBTC, 5.12, 'long'), + ], + orderType: 'two-way', + quoteAsset: await SpotQuoteInstrument.create(takerCvg, quoteMint), + fixedSize, + }); + const { rfqResponse } = await makerCvg.rfqs().respond({ + rfq: rfq.address, + bid: { + price: 12000, + }, + ask: { + price: 21000, + }, + }); + + await takerCvg.rfqs().confirmResponse({ + response: rfqResponse.address, + side: 'bid', + rfq: rfq.address, + }); + + const refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ + address: rfqResponse.address, + }); + //Approve + expect( + takerCvg.rfqs().getResponseState({ + response: refreshedResponse, + rfq, + caller: 'taker', + responseSide: 'bid', + }).responseState + ).toBe('Settle'); + + //Rejected + expect( + takerCvg.rfqs().getResponseState({ + response: refreshedResponse, + rfq, + caller: 'taker', + responseSide: 'ask', + }).responseState + ).toBe('Rejected'); + }); +}); From 3333200711434a218fb0b624045529bc1b251d5c Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Tue, 29 Aug 2023 20:09:38 +0530 Subject: [PATCH 02/11] fix typos and add correct comments --- .../rfqModule/operations/getResponseState.ts | 4 ++-- packages/js/tests/unit/responseState.spec.ts | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseState.ts b/packages/js/src/plugins/rfqModule/operations/getResponseState.ts index 933f109d9..e206dd544 100644 --- a/packages/js/src/plugins/rfqModule/operations/getResponseState.ts +++ b/packages/js/src/plugins/rfqModule/operations/getResponseState.ts @@ -77,7 +77,7 @@ export const getResponseStateHandler: SyncOperationHandler = { timestampExpiry.getTime() + Number(rfq.settlingWindow) * 1000 ); const rfqExpired = timestampExpiry.getTime() <= Date.now(); - const settlememtWindowElapsed = timestampSettlement.getTime() <= Date.now(); + const settlementWindowElapsed = timestampSettlement.getTime() <= Date.now(); const confirmedResponseSide = (responseSide === 'ask' && response?.confirmed?.side === 'ask') || (responseSide === 'bid' && response?.confirmed?.side === 'bid'); @@ -101,7 +101,7 @@ export const getResponseStateHandler: SyncOperationHandler = { const defaulted = response.defaultingParty ? response.defaultingParty !== null - : settlememtWindowElapsed && + : settlementWindowElapsed && (!partyPreparedLegsComplete || !counterpartyPreparedLegsComplete); const responseConfirmed = response?.confirmed !== null; diff --git a/packages/js/tests/unit/responseState.spec.ts b/packages/js/tests/unit/responseState.spec.ts index cea5ccdb8..38658945b 100644 --- a/packages/js/tests/unit/responseState.spec.ts +++ b/packages/js/tests/unit/responseState.spec.ts @@ -9,7 +9,7 @@ import { import { createUserCvg, sleep } from '../helpers'; import { BASE_MINT_BTC_PK, QUOTE_MINT_PK } from '../constants'; -describe('unit.getResponseState', () => { +describe('unit.ResponseState', () => { const takerCvg = createUserCvg('taker'); const makerCvg = createUserCvg('maker'); @@ -71,7 +71,7 @@ describe('unit.getResponseState', () => { }).responseState ).toBe('Cancelled'); - //Unlock Response Collateral + //Unlock Response Collateral for maker refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ address: rfqResponse.address, }); @@ -127,7 +127,7 @@ describe('unit.getResponseState', () => { }, }); - //Approve + //Approve for taker expect( takerCvg.rfqs().getResponseState({ response: rfqResponse, @@ -185,7 +185,7 @@ describe('unit.getResponseState', () => { taker: takerCvg.identity().publicKey, }); - //Settled + //Settled for maker refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ address: rfqResponse.address, }); @@ -221,7 +221,8 @@ describe('unit.getResponseState', () => { }, }); await sleep(3); - //Expired + + //Expired for taker expect( takerCvg.rfqs().getResponseState({ response: rfqResponse, @@ -264,7 +265,8 @@ describe('unit.getResponseState', () => { const refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ address: rfqResponse.address, }); - //Approve + + //Approve for taker expect( takerCvg.rfqs().getResponseState({ response: refreshedResponse, @@ -274,7 +276,7 @@ describe('unit.getResponseState', () => { }).responseState ).toBe('Settle'); - //Rejected + //Rejected for maker expect( takerCvg.rfqs().getResponseState({ response: refreshedResponse, From 5b7b12fa87ea4d3afad8f6965de4ecd5c3833933 Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Thu, 31 Aug 2023 14:05:20 +0530 Subject: [PATCH 03/11] change getResponseState operation --- .../js/src/plugins/rfqModule/RfqClient.ts | 12 +- .../rfqModule/operations/getResponseState.ts | 184 -------------- .../operations/getResponseStateAndAction.ts | 233 ++++++++++++++++++ .../src/plugins/rfqModule/operations/index.ts | 2 +- packages/js/src/plugins/rfqModule/plugin.ts | 9 +- ...spec.ts => responseStateAndAction.spec.ts} | 46 ++-- 6 files changed, 269 insertions(+), 217 deletions(-) delete mode 100644 packages/js/src/plugins/rfqModule/operations/getResponseState.ts create mode 100644 packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts rename packages/js/tests/unit/{responseState.spec.ts => responseStateAndAction.spec.ts} (88%) diff --git a/packages/js/src/plugins/rfqModule/RfqClient.ts b/packages/js/src/plugins/rfqModule/RfqClient.ts index 932c88267..6ff430c8a 100644 --- a/packages/js/src/plugins/rfqModule/RfqClient.ts +++ b/packages/js/src/plugins/rfqModule/RfqClient.ts @@ -79,9 +79,9 @@ import { GetSettlementResultInput, getSettlementResultOperation, getSettlementResultHandler, - GetResponseStateInput, - getResponseStateOperation, - getResponseStateHandler, + GetResponseStateAndActionInput, + getResponseStateAndActionOperation, + getResponseStateAndActionHandler, } from './operations'; import { Response } from './models/Response'; @@ -493,9 +493,9 @@ export class RfqClient { } /** {@inheritDoc getResponseState} */ - getResponseState(input: GetResponseStateInput) { - return getResponseStateHandler.handle( - getResponseStateOperation(input), + getResponseStateAndAction(input: GetResponseStateAndActionInput) { + return getResponseStateAndActionHandler.handle( + getResponseStateAndActionOperation(input), this.convergence ); } diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseState.ts b/packages/js/src/plugins/rfqModule/operations/getResponseState.ts deleted file mode 100644 index e206dd544..000000000 --- a/packages/js/src/plugins/rfqModule/operations/getResponseState.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Rfq, Response } from '../models'; -import { Operation, SyncOperationHandler, useOperation } from '../../../types'; -const Key = 'GetResponseState' as const; -export type ResponseState = - | 'Kill' - | 'Reclaim' - | 'Cleanup' - | 'Rejected' - | 'Defaulted' - | 'Settle' - | 'Settled' - | 'Expired' - | 'Approve' - | 'Cancelled' - | 'None'; -/** - * getResponseState. - * - * ```ts - * const result = await convergence.rfqs().getResponseState({ - * response, - * rfq, - * }) - * ``` - * - * @group Operations - * @category Constructors - */ -export const getResponseStateOperation = useOperation(Key); - -/** - * @group Operations - * @category Types - */ -export type GetResponseState = Operation< - typeof Key, - GetResponseStateInput, - GetResponseStateOutput ->; - -/** - * @group Operations - * @category Inputs - */ -export type GetResponseStateInput = { - response: Response; - rfq: Rfq; - caller: 'taker' | 'maker'; - responseSide: 'ask' | 'bid'; -}; - -/** - * @group Operations - * @category Outputs - */ -export type GetResponseStateOutput = { - responseState: ResponseState; -}; - -/** - * @group Operations - * @category Handlers - */ -export const getResponseStateHandler: SyncOperationHandler = { - handle: (operation: GetResponseState): GetResponseStateOutput => { - const { response, caller, rfq, responseSide } = operation.input; - if (!response.rfq.equals(rfq.address)) { - throw new Error('Response does not match RFQ'); - } - let state: ResponseState; - const responseState = response.state; - const timestampStart = new Date(Number(rfq.creationTimestamp)); - const timestampExpiry = new Date( - timestampStart.getTime() + Number(rfq.activeWindow) * 1000 - ); - const timestampSettlement = new Date( - timestampExpiry.getTime() + Number(rfq.settlingWindow) * 1000 - ); - const rfqExpired = timestampExpiry.getTime() <= Date.now(); - const settlementWindowElapsed = timestampSettlement.getTime() <= Date.now(); - const confirmedResponseSide = - (responseSide === 'ask' && response?.confirmed?.side === 'ask') || - (responseSide === 'bid' && response?.confirmed?.side === 'bid'); - - const confirmedInverseResponseSide = - (responseSide === 'ask' && response?.confirmed?.side !== 'ask') || - (responseSide === 'bid' && response?.confirmed?.side !== 'bid'); - - const takerPreparedLegsComplete = - response.takerPreparedLegs === rfq.legs.length; - const makerPreparedLegsComplete = - response.makerPreparedLegs === rfq.legs.length; - const partyPreparedLegsComplete = - caller === 'maker' - ? makerPreparedLegsComplete - : takerPreparedLegsComplete; - const counterpartyPreparedLegsComplete = - caller === 'maker' - ? takerPreparedLegsComplete - : makerPreparedLegsComplete; - - const defaulted = response.defaultingParty - ? response.defaultingParty !== null - : settlementWindowElapsed && - (!partyPreparedLegsComplete || !counterpartyPreparedLegsComplete); - - const responseConfirmed = response?.confirmed !== null; - switch (caller) { - case 'maker': - switch (true) { - case responseState === 'active' && !responseConfirmed && !rfqExpired: - state = 'Kill'; - break; - case ((responseState === 'active' && - !responseConfirmed && - rfqExpired) || - responseState === 'canceled') && - response.makerCollateralLocked > 0: - state = 'Reclaim'; - break; - case (responseState === 'active' || responseState === 'canceled') && - response.makerCollateralLocked === 0: - state = 'Cleanup'; - break; - case responseConfirmed && confirmedInverseResponseSide: - state = 'Rejected'; - break; - case confirmedResponseSide && - (responseState === 'settling-preparations' || - responseState === 'ready-for-settling') && - !defaulted: - state = 'Settle'; - break; - case defaulted || responseState === 'defaulted': - state = 'Defaulted'; - break; - case !defaulted && responseState === 'settled': - state = 'Settled'; - break; - default: - state = 'None'; - break; - } - break; - - case 'taker': - switch (true) { - case responseState === 'active' && rfqExpired && !responseConfirmed: - state = 'Expired'; - break; - case responseState === 'canceled': - state = 'Cancelled'; - break; - case responseConfirmed && confirmedInverseResponseSide: - state = 'Rejected'; - break; - case responseState === 'active' && !rfqExpired && !responseConfirmed: - state = 'Approve'; - break; - case confirmedResponseSide && - (responseState === 'settling-preparations' || - responseState === 'ready-for-settling') && - !defaulted: - state = 'Settle'; - break; - case defaulted || responseState === 'defaulted': - state = 'Defaulted'; - break; - case responseState === 'settled': - state = 'Settled'; - break; - default: - state = 'None'; - break; - } - break; - default: - state = 'None'; - break; - } - - return { responseState: state }; - }, -}; diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts new file mode 100644 index 000000000..08634c114 --- /dev/null +++ b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts @@ -0,0 +1,233 @@ +import { Rfq, Response } from '../models'; +import { Operation, SyncOperationHandler, useOperation } from '../../../types'; +const Key = 'GetResponseStateAndAction' as const; +export type ResponseAction = + | 'Cancel' + | 'UnlockCollatral' + | 'Cleanup' + | 'Rejected' + | 'Defaulted' + | 'Settle' + | 'Settled' + | 'Expired' + | 'Approve' + | 'Cancelled' + | null; + +export type ResponseState = + | 'Active' + | 'Cancelled' + | 'Defaulted' + | 'Settled' + | 'SettlingPreparations' + | 'ReadyForSettling' + | 'WaitingForLastLook' + | null; + +/** + * getResponseStateAndAction. + * + * ```ts + * const result = await convergence.rfqs().getResponseStateAndAction({ + * response, + * rfq, + * }) + * ``` + * + * @group Operations + * @category Constructors + */ +export const getResponseStateAndActionOperation = + useOperation(Key); + +/** + * @group Operations + * @category Types + */ +export type GetResponseStateAndAction = Operation< + typeof Key, + GetResponseStateAndActionInput, + GetResponseStateAndActionOutput +>; + +/** + * @group Operations + * @category Inputs + */ +export type GetResponseStateAndActionInput = { + response: Response; + rfq: Rfq; + caller: 'taker' | 'maker'; + responseSide: 'ask' | 'bid'; +}; + +/** + * @group Operations + * @category Outputs + */ +export type GetResponseStateAndActionOutput = { + responseState: ResponseState; + responseAction: ResponseAction; +}; + +/** + * @group Operations + * @category Handlers + */ +export const getResponseStateAndActionHandler: SyncOperationHandler = + { + handle: ( + operation: GetResponseStateAndAction + ): GetResponseStateAndActionOutput => { + const { response, caller, rfq, responseSide } = operation.input; + let responseState: ResponseState; + switch (true) { + case response.state === 'active': + responseState = 'Active'; + break; + case response.state === 'settling-preparations': + responseState = 'SettlingPreparations'; + break; + case response.state === 'ready-for-settling': + responseState = 'ReadyForSettling'; + break; + case response.state === 'settled': + responseState = 'Settled'; + break; + case response.state === 'defaulted': + responseState = 'Defaulted'; + break; + case response.state === 'canceled': + responseState = 'Cancelled'; + break; + case response.state === 'waiting-for-last-look': + responseState = 'WaitingForLastLook'; + break; + default: + responseState = null; + break; + } + + if (!response.rfq.equals(rfq.address)) { + throw new Error('Response does not match RFQ'); + } + let responseAction: ResponseAction; + const timestampStart = new Date(Number(rfq.creationTimestamp)); + const timestampExpiry = new Date( + timestampStart.getTime() + Number(rfq.activeWindow) * 1000 + ); + const timestampSettlement = new Date( + timestampExpiry.getTime() + Number(rfq.settlingWindow) * 1000 + ); + const rfqExpired = timestampExpiry.getTime() <= Date.now(); + const settlementWindowElapsed = + timestampSettlement.getTime() <= Date.now(); + const confirmedResponseSide = + (responseSide === 'ask' && response?.confirmed?.side === 'ask') || + (responseSide === 'bid' && response?.confirmed?.side === 'bid'); + + const confirmedInverseResponseSide = + (responseSide === 'ask' && response?.confirmed?.side !== 'ask') || + (responseSide === 'bid' && response?.confirmed?.side !== 'bid'); + + const takerPreparedLegsComplete = + response.takerPreparedLegs === rfq.legs.length; + const makerPreparedLegsComplete = + response.makerPreparedLegs === rfq.legs.length; + const partyPreparedLegsComplete = + caller === 'maker' + ? makerPreparedLegsComplete + : takerPreparedLegsComplete; + const counterpartyPreparedLegsComplete = + caller === 'maker' + ? takerPreparedLegsComplete + : makerPreparedLegsComplete; + + const defaulted = response.defaultingParty + ? response.defaultingParty !== null + : settlementWindowElapsed && + (!partyPreparedLegsComplete || !counterpartyPreparedLegsComplete); + + const responseConfirmed = response?.confirmed !== null; + switch (caller) { + case 'maker': + switch (true) { + case responseState === 'Active' && + !responseConfirmed && + !rfqExpired: + responseAction = 'Cancel'; + break; + case ((responseState === 'Active' && + !responseConfirmed && + rfqExpired) || + responseState === 'Cancelled') && + response.makerCollateralLocked > 0: + responseAction = 'UnlockCollatral'; + break; + case (responseState === 'Active' || + responseState === 'Cancelled') && + response.makerCollateralLocked === 0: + responseAction = 'Cleanup'; + break; + case responseConfirmed && confirmedInverseResponseSide: + responseAction = 'Rejected'; + break; + case confirmedResponseSide && + (responseState === 'SettlingPreparations' || + responseState === 'ReadyForSettling') && + !defaulted: + responseAction = 'Settle'; + break; + case defaulted || responseState === 'Defaulted': + responseAction = 'Defaulted'; + break; + case !defaulted && responseState === 'Settled': + responseAction = 'Settled'; + break; + default: + responseAction = null; + break; + } + break; + + case 'taker': + switch (true) { + case responseState === 'Active' && rfqExpired && !responseConfirmed: + responseAction = 'Expired'; + break; + case responseState === 'Cancelled': + responseAction = 'Cancelled'; + break; + case responseConfirmed && confirmedInverseResponseSide: + responseAction = 'Rejected'; + break; + case responseState === 'Active' && + !rfqExpired && + !responseConfirmed: + responseAction = 'Approve'; + break; + case confirmedResponseSide && + (responseState === 'SettlingPreparations' || + responseState === 'ReadyForSettling') && + !defaulted: + responseAction = 'Settle'; + break; + case defaulted || responseState === 'Defaulted': + responseAction = 'Defaulted'; + break; + case responseState === 'Settled': + responseAction = 'Settled'; + break; + default: + responseAction = null; + break; + } + break; + default: + responseAction = null; + break; + } + + return { responseState, responseAction }; + }, + }; diff --git a/packages/js/src/plugins/rfqModule/operations/index.ts b/packages/js/src/plugins/rfqModule/operations/index.ts index d03203391..d82acb2ea 100644 --- a/packages/js/src/plugins/rfqModule/operations/index.ts +++ b/packages/js/src/plugins/rfqModule/operations/index.ts @@ -35,4 +35,4 @@ export * from './unlockResponseCollateral'; export * from './unlockResponsesCollateral'; export * from './unlockRfqsCollateral'; export * from './getSettlementResult'; -export * from './getResponseState'; +export * from './getResponseStateAndAction'; diff --git a/packages/js/src/plugins/rfqModule/plugin.ts b/packages/js/src/plugins/rfqModule/plugin.ts index 286036205..9779d2ba2 100644 --- a/packages/js/src/plugins/rfqModule/plugin.ts +++ b/packages/js/src/plugins/rfqModule/plugin.ts @@ -73,8 +73,8 @@ import { cleanUpRfqsOperationHandler, getSettlementResultOperation, getSettlementResultHandler, - getResponseStateOperation, - getResponseStateHandler, + getResponseStateAndActionOperation, + getResponseStateAndActionHandler, } from './operations'; import { rfqProgram } from './program'; @@ -184,7 +184,10 @@ export const rfqModule = (): ConvergencePlugin => ({ unlockRfqsCollateralOperationHandler ); op.register(getSettlementResultOperation, getSettlementResultHandler); - op.register(getResponseStateOperation, getResponseStateHandler); + op.register( + getResponseStateAndActionOperation, + getResponseStateAndActionHandler + ); convergence.rfqs = function () { return new RfqClient(this); diff --git a/packages/js/tests/unit/responseState.spec.ts b/packages/js/tests/unit/responseStateAndAction.spec.ts similarity index 88% rename from packages/js/tests/unit/responseState.spec.ts rename to packages/js/tests/unit/responseStateAndAction.spec.ts index 38658945b..44757183f 100644 --- a/packages/js/tests/unit/responseState.spec.ts +++ b/packages/js/tests/unit/responseStateAndAction.spec.ts @@ -9,7 +9,7 @@ import { import { createUserCvg, sleep } from '../helpers'; import { BASE_MINT_BTC_PK, QUOTE_MINT_PK } from '../constants'; -describe('unit.ResponseState', () => { +describe('unit.ResponseStateAndAction', () => { const takerCvg = createUserCvg('taker'); const makerCvg = createUserCvg('maker'); @@ -48,12 +48,12 @@ describe('unit.ResponseState', () => { //Kill for maker expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: rfqResponse, rfq, caller: 'maker', responseSide: 'bid', - }).responseState + }).responseAction ).toBe('Kill'); await makerCvg.rfqs().cancelResponse({ response: rfqResponse.address }); @@ -63,12 +63,12 @@ describe('unit.ResponseState', () => { //Cancelled for taker expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, rfq, caller: 'taker', responseSide: 'bid', - }).responseState + }).responseAction ).toBe('Cancelled'); //Unlock Response Collateral for maker @@ -76,12 +76,12 @@ describe('unit.ResponseState', () => { address: rfqResponse.address, }); expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, rfq, caller: 'maker', responseSide: 'bid', - }).responseState + }).responseAction ).toBe('Reclaim'); await makerCvg.rfqs().unlockResponseCollateral({ response: refreshedResponse.address, @@ -92,12 +92,12 @@ describe('unit.ResponseState', () => { address: rfqResponse.address, }); expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, rfq, caller: 'maker', responseSide: 'bid', - }).responseState + }).responseAction ).toBe('Cleanup'); await makerCvg.rfqs().cleanUpResponse({ @@ -129,12 +129,12 @@ describe('unit.ResponseState', () => { //Approve for taker expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: rfqResponse, rfq, caller: 'taker', responseSide: 'bid', - }).responseState + }).responseAction ).toBe('Approve'); await takerCvg.rfqs().confirmResponse({ @@ -148,22 +148,22 @@ describe('unit.ResponseState', () => { address: rfqResponse.address, }); expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, rfq, caller: 'maker', responseSide: 'bid', - }).responseState + }).responseAction ).toBe('Settle'); //Settle for taker expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, rfq, caller: 'taker', responseSide: 'bid', - }).responseState + }).responseAction ).toBe('Settle'); await takerCvg.rfqs().prepareSettlement({ @@ -190,12 +190,12 @@ describe('unit.ResponseState', () => { address: rfqResponse.address, }); expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, rfq, caller: 'maker', responseSide: 'bid', - }).responseState + }).responseAction ).toBe('Settled'); }); @@ -224,12 +224,12 @@ describe('unit.ResponseState', () => { //Expired for taker expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: rfqResponse, rfq, caller: 'taker', responseSide: 'bid', - }).responseState + }).responseAction ).toBe('Expired'); }); @@ -268,22 +268,22 @@ describe('unit.ResponseState', () => { //Approve for taker expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, rfq, caller: 'taker', responseSide: 'bid', - }).responseState + }).responseAction ).toBe('Settle'); //Rejected for maker expect( - takerCvg.rfqs().getResponseState({ + takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, rfq, caller: 'taker', responseSide: 'ask', - }).responseState + }).responseAction ).toBe('Rejected'); }); }); From 69a68340e5628176df077fe7fcf950b1bc42215e Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Thu, 31 Aug 2023 14:10:32 +0530 Subject: [PATCH 04/11] fix typos --- .../rfqModule/operations/getResponseStateAndAction.ts | 4 ++-- packages/js/tests/unit/responseStateAndAction.spec.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts index 08634c114..a5538fe7c 100644 --- a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts +++ b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts @@ -3,7 +3,7 @@ import { Operation, SyncOperationHandler, useOperation } from '../../../types'; const Key = 'GetResponseStateAndAction' as const; export type ResponseAction = | 'Cancel' - | 'UnlockCollatral' + | 'UnlockCollateral' | 'Cleanup' | 'Rejected' | 'Defaulted' @@ -162,7 +162,7 @@ export const getResponseStateAndActionHandler: SyncOperationHandler 0: - responseAction = 'UnlockCollatral'; + responseAction = 'UnlockCollateral'; break; case (responseState === 'Active' || responseState === 'Cancelled') && diff --git a/packages/js/tests/unit/responseStateAndAction.spec.ts b/packages/js/tests/unit/responseStateAndAction.spec.ts index 44757183f..2e2b145e6 100644 --- a/packages/js/tests/unit/responseStateAndAction.spec.ts +++ b/packages/js/tests/unit/responseStateAndAction.spec.ts @@ -9,7 +9,7 @@ import { import { createUserCvg, sleep } from '../helpers'; import { BASE_MINT_BTC_PK, QUOTE_MINT_PK } from '../constants'; -describe('unit.ResponseStateAndAction', () => { +describe('unit.responseStateAndAction', () => { const takerCvg = createUserCvg('taker'); const makerCvg = createUserCvg('maker'); @@ -25,7 +25,7 @@ describe('unit.ResponseStateAndAction', () => { .findMintByAddress({ address: QUOTE_MINT_PK }); }); - it('[Kill, Reclaim, Cleanup, Cancelled]', async () => { + it('[Cancel, UnlockCollateral, Cleanup, Cancelled]', async () => { let refreshedResponse: Response; const fixedSize: FixedSize = { type: 'fixed-base', @@ -46,7 +46,7 @@ describe('unit.ResponseStateAndAction', () => { }, }); - //Kill for maker + //Cancel for maker expect( takerCvg.rfqs().getResponseStateAndAction({ response: rfqResponse, @@ -54,7 +54,7 @@ describe('unit.ResponseStateAndAction', () => { caller: 'maker', responseSide: 'bid', }).responseAction - ).toBe('Kill'); + ).toBe('Cancel'); await makerCvg.rfqs().cancelResponse({ response: rfqResponse.address }); refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ @@ -82,7 +82,7 @@ describe('unit.ResponseStateAndAction', () => { caller: 'maker', responseSide: 'bid', }).responseAction - ).toBe('Reclaim'); + ).toBe('UnlockCollateral'); await makerCvg.rfqs().unlockResponseCollateral({ response: refreshedResponse.address, }); From 5b96b66fd6b650ea32392f7dbd6303092d5b5a23 Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Thu, 31 Aug 2023 15:35:35 +0530 Subject: [PATCH 05/11] refactor getResponseState to decrease complexity and make use of responseState and responseAction --- .../operations/getResponseStateAndAction.ts | 266 ++++++++---------- 1 file changed, 121 insertions(+), 145 deletions(-) diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts index a5538fe7c..65273cf3f 100644 --- a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts +++ b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts @@ -29,8 +29,10 @@ export type ResponseState = * * ```ts * const result = await convergence.rfqs().getResponseStateAndAction({ - * response, + * response, * rfq, + * caller: 'taker'| 'maker, + * responseSide: 'ask' | 'bid' * }) * ``` * @@ -80,154 +82,128 @@ export const getResponseStateAndActionHandler: SyncOperationHandler { const { response, caller, rfq, responseSide } = operation.input; - let responseState: ResponseState; - switch (true) { - case response.state === 'active': - responseState = 'Active'; - break; - case response.state === 'settling-preparations': - responseState = 'SettlingPreparations'; - break; - case response.state === 'ready-for-settling': - responseState = 'ReadyForSettling'; - break; - case response.state === 'settled': - responseState = 'Settled'; - break; - case response.state === 'defaulted': - responseState = 'Defaulted'; - break; - case response.state === 'canceled': - responseState = 'Cancelled'; - break; - case response.state === 'waiting-for-last-look': - responseState = 'WaitingForLastLook'; - break; - default: - responseState = null; - break; - } - if (!response.rfq.equals(rfq.address)) { throw new Error('Response does not match RFQ'); } - let responseAction: ResponseAction; - const timestampStart = new Date(Number(rfq.creationTimestamp)); - const timestampExpiry = new Date( - timestampStart.getTime() + Number(rfq.activeWindow) * 1000 - ); - const timestampSettlement = new Date( - timestampExpiry.getTime() + Number(rfq.settlingWindow) * 1000 - ); - const rfqExpired = timestampExpiry.getTime() <= Date.now(); - const settlementWindowElapsed = - timestampSettlement.getTime() <= Date.now(); - const confirmedResponseSide = - (responseSide === 'ask' && response?.confirmed?.side === 'ask') || - (responseSide === 'bid' && response?.confirmed?.side === 'bid'); - - const confirmedInverseResponseSide = - (responseSide === 'ask' && response?.confirmed?.side !== 'ask') || - (responseSide === 'bid' && response?.confirmed?.side !== 'bid'); - - const takerPreparedLegsComplete = - response.takerPreparedLegs === rfq.legs.length; - const makerPreparedLegsComplete = - response.makerPreparedLegs === rfq.legs.length; - const partyPreparedLegsComplete = - caller === 'maker' - ? makerPreparedLegsComplete - : takerPreparedLegsComplete; - const counterpartyPreparedLegsComplete = - caller === 'maker' - ? takerPreparedLegsComplete - : makerPreparedLegsComplete; - - const defaulted = response.defaultingParty - ? response.defaultingParty !== null - : settlementWindowElapsed && - (!partyPreparedLegsComplete || !counterpartyPreparedLegsComplete); - - const responseConfirmed = response?.confirmed !== null; - switch (caller) { - case 'maker': - switch (true) { - case responseState === 'Active' && - !responseConfirmed && - !rfqExpired: - responseAction = 'Cancel'; - break; - case ((responseState === 'Active' && - !responseConfirmed && - rfqExpired) || - responseState === 'Cancelled') && - response.makerCollateralLocked > 0: - responseAction = 'UnlockCollateral'; - break; - case (responseState === 'Active' || - responseState === 'Cancelled') && - response.makerCollateralLocked === 0: - responseAction = 'Cleanup'; - break; - case responseConfirmed && confirmedInverseResponseSide: - responseAction = 'Rejected'; - break; - case confirmedResponseSide && - (responseState === 'SettlingPreparations' || - responseState === 'ReadyForSettling') && - !defaulted: - responseAction = 'Settle'; - break; - case defaulted || responseState === 'Defaulted': - responseAction = 'Defaulted'; - break; - case !defaulted && responseState === 'Settled': - responseAction = 'Settled'; - break; - default: - responseAction = null; - break; - } - break; - - case 'taker': - switch (true) { - case responseState === 'Active' && rfqExpired && !responseConfirmed: - responseAction = 'Expired'; - break; - case responseState === 'Cancelled': - responseAction = 'Cancelled'; - break; - case responseConfirmed && confirmedInverseResponseSide: - responseAction = 'Rejected'; - break; - case responseState === 'Active' && - !rfqExpired && - !responseConfirmed: - responseAction = 'Approve'; - break; - case confirmedResponseSide && - (responseState === 'SettlingPreparations' || - responseState === 'ReadyForSettling') && - !defaulted: - responseAction = 'Settle'; - break; - case defaulted || responseState === 'Defaulted': - responseAction = 'Defaulted'; - break; - case responseState === 'Settled': - responseAction = 'Settled'; - break; - default: - responseAction = null; - break; - } - break; - default: - responseAction = null; - break; - } + const responseState = getResponseState(response); + const responseAction = getResponseAction( + response, + rfq, + responseSide, + caller, + responseState + ); return { responseState, responseAction }; }, }; + +const getResponseState = (response: Response): ResponseState => { + if (response.state === 'active') return 'Active'; + if (response.state === 'settling-preparations') return 'SettlingPreparations'; + if (response.state === 'ready-for-settling') return 'ReadyForSettling'; + if (response.state === 'settled') return 'Settled'; + if (response.state === 'defaulted') return 'Defaulted'; + if (response.state === 'canceled') return 'Cancelled'; + if (response.state === 'waiting-for-last-look') return 'WaitingForLastLook'; + return null; +}; + +const getResponseAction = ( + response: Response, + rfq: Rfq, + responseSide: 'ask' | 'bid', + caller: 'maker' | 'taker', + responseState: ResponseState +): ResponseAction => { + const timestampStart = new Date(Number(rfq.creationTimestamp)); + const timestampExpiry = new Date( + timestampStart.getTime() + Number(rfq.activeWindow) * 1000 + ); + const timestampSettlement = new Date( + timestampExpiry.getTime() + Number(rfq.settlingWindow) * 1000 + ); + const rfqExpired = timestampExpiry.getTime() <= Date.now(); + const settlementWindowElapsed = timestampSettlement.getTime() <= Date.now(); + const confirmedResponseSide = + (responseSide === 'ask' && response?.confirmed?.side === 'ask') || + (responseSide === 'bid' && response?.confirmed?.side === 'bid'); + + const confirmedInverseResponseSide = + (responseSide === 'ask' && response?.confirmed?.side !== 'ask') || + (responseSide === 'bid' && response?.confirmed?.side !== 'bid'); + + const takerPreparedLegsComplete = + response.takerPreparedLegs === rfq.legs.length; + const makerPreparedLegsComplete = + response.makerPreparedLegs === rfq.legs.length; + const partyPreparedLegsComplete = + caller === 'maker' ? makerPreparedLegsComplete : takerPreparedLegsComplete; + const counterpartyPreparedLegsComplete = + caller === 'maker' ? takerPreparedLegsComplete : makerPreparedLegsComplete; + + const defaulted = response.defaultingParty + ? response.defaultingParty !== null + : settlementWindowElapsed && + (!partyPreparedLegsComplete || !counterpartyPreparedLegsComplete); + + const responseConfirmed = response?.confirmed !== null; + + if (caller === 'maker') { + if (responseState === 'Active' && !responseConfirmed && !rfqExpired) + return 'Cancel'; + + if ( + (responseState === 'Active' && !responseConfirmed && rfqExpired) || + (responseState === 'Cancelled' && response.makerCollateralLocked > 0) + ) + return 'UnlockCollateral'; + + if ( + (responseState === 'Active' || responseState === 'Cancelled') && + response.makerCollateralLocked === 0 + ) + return 'Cleanup'; + + if (responseConfirmed && confirmedInverseResponseSide) return 'Rejected'; + + if ( + confirmedResponseSide && + (responseState === 'SettlingPreparations' || + responseState === 'ReadyForSettling') && + !defaulted + ) + return 'Settle'; + + if (defaulted || responseState === 'Defaulted') return 'Defaulted'; + + if (!defaulted && responseState === 'Settled') return 'Settled'; + + return null; + } else if (caller === 'taker') { + if (responseState === 'Active' && rfqExpired && !responseConfirmed) + return 'Expired'; + + if (responseState === 'Cancelled') return 'Cancelled'; + + if (responseConfirmed && confirmedInverseResponseSide) return 'Rejected'; + + if (responseState === 'Active' && !rfqExpired && !responseConfirmed) + return 'Approve'; + + if ( + confirmedResponseSide && + (responseState === 'SettlingPreparations' || + responseState === 'ReadyForSettling') && + !defaulted + ) + return 'Settle'; + + if (defaulted || responseState === 'Defaulted') return 'Defaulted'; + + if (responseState === 'Settled') return 'Settled'; + + return null; + } + return null; +}; From 1025204ced8cb647e3523c85c397217dbbe485f3 Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Mon, 4 Sep 2023 12:55:31 +0530 Subject: [PATCH 06/11] refactor getResponseStateAndActionto improve code readability --- .../operations/getResponseStateAndAction.ts | 213 ++++++++++-------- .../tests/unit/responseStateAndAction.spec.ts | 32 ++- 2 files changed, 156 insertions(+), 89 deletions(-) diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts index 65273cf3f..bdf1379a8 100644 --- a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts +++ b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts @@ -1,6 +1,18 @@ import { Rfq, Response } from '../models'; import { Operation, SyncOperationHandler, useOperation } from '../../../types'; const Key = 'GetResponseStateAndAction' as const; +export type ResponseState = + | 'Active' + | 'Cancelled' + | 'Expired' + | 'Defaulted' + | 'Settled' + | 'SettlingPreparations' + | 'ReadyForSettling' + | 'WaitingForLastLook' + | 'OnlyMakerPrepared' + | 'OnlyTakerPrepared'; + export type ResponseAction = | 'Cancel' | 'UnlockCollateral' @@ -14,16 +26,6 @@ export type ResponseAction = | 'Cancelled' | null; -export type ResponseState = - | 'Active' - | 'Cancelled' - | 'Defaulted' - | 'Settled' - | 'SettlingPreparations' - | 'ReadyForSettling' - | 'WaitingForLastLook' - | null; - /** * getResponseStateAndAction. * @@ -85,28 +87,66 @@ export const getResponseStateAndActionHandler: SyncOperationHandler { - if (response.state === 'active') return 'Active'; - if (response.state === 'settling-preparations') return 'SettlingPreparations'; - if (response.state === 'ready-for-settling') return 'ReadyForSettling'; - if (response.state === 'settled') return 'Settled'; - if (response.state === 'defaulted') return 'Defaulted'; - if (response.state === 'canceled') return 'Cancelled'; - if (response.state === 'waiting-for-last-look') return 'WaitingForLastLook'; - return null; +const getResponseState = ( + response: Response, + rfq: Rfq, + timestampExpiry: Date, + timestampSettlement: Date +): ResponseState => { + const rfqExpired = timestampExpiry.getTime() <= Date.now(); + const settlementWindowElapsed = timestampSettlement.getTime() <= Date.now(); + switch (response.state) { + case 'active': + if (!rfqExpired) return 'Active'; + return 'Expired'; + case 'canceled': + return 'Cancelled'; + case 'waiting-for-last-look': + if (!rfqExpired) return 'WaitingForLastLook'; + return 'Expired'; + case 'settling-preparations': + if (!settlementWindowElapsed) { + if (response.makerPreparedLegs === rfq.legs.length) + return 'OnlyMakerPrepared'; + if (response.takerPreparedLegs === rfq.legs.length) + return 'OnlyTakerPrepared'; + return 'SettlingPreparations'; + } + return 'Defaulted'; + case 'ready-for-settling': + return 'ReadyForSettling'; + case 'settled': + return 'Settled'; + case 'defaulted': + return 'Defaulted'; + default: + throw new Error('Invalid Response state'); + } }; const getResponseAction = ( @@ -114,17 +154,9 @@ const getResponseAction = ( rfq: Rfq, responseSide: 'ask' | 'bid', caller: 'maker' | 'taker', - responseState: ResponseState + responseState: ResponseState, + timestampSettlement: Date ): ResponseAction => { - const timestampStart = new Date(Number(rfq.creationTimestamp)); - const timestampExpiry = new Date( - timestampStart.getTime() + Number(rfq.activeWindow) * 1000 - ); - const timestampSettlement = new Date( - timestampExpiry.getTime() + Number(rfq.settlingWindow) * 1000 - ); - const rfqExpired = timestampExpiry.getTime() <= Date.now(); - const settlementWindowElapsed = timestampSettlement.getTime() <= Date.now(); const confirmedResponseSide = (responseSide === 'ask' && response?.confirmed?.side === 'ask') || (responseSide === 'bid' && response?.confirmed?.side === 'bid'); @@ -142,68 +174,73 @@ const getResponseAction = ( const counterpartyPreparedLegsComplete = caller === 'maker' ? takerPreparedLegsComplete : makerPreparedLegsComplete; + const settlementWindowElapsed = timestampSettlement.getTime() <= Date.now(); const defaulted = response.defaultingParty ? response.defaultingParty !== null : settlementWindowElapsed && (!partyPreparedLegsComplete || !counterpartyPreparedLegsComplete); const responseConfirmed = response?.confirmed !== null; - - if (caller === 'maker') { - if (responseState === 'Active' && !responseConfirmed && !rfqExpired) - return 'Cancel'; - - if ( - (responseState === 'Active' && !responseConfirmed && rfqExpired) || - (responseState === 'Cancelled' && response.makerCollateralLocked > 0) - ) - return 'UnlockCollateral'; - - if ( - (responseState === 'Active' || responseState === 'Cancelled') && - response.makerCollateralLocked === 0 - ) - return 'Cleanup'; - - if (responseConfirmed && confirmedInverseResponseSide) return 'Rejected'; - - if ( - confirmedResponseSide && - (responseState === 'SettlingPreparations' || - responseState === 'ReadyForSettling') && - !defaulted - ) - return 'Settle'; - - if (defaulted || responseState === 'Defaulted') return 'Defaulted'; - - if (!defaulted && responseState === 'Settled') return 'Settled'; - - return null; - } else if (caller === 'taker') { - if (responseState === 'Active' && rfqExpired && !responseConfirmed) - return 'Expired'; - - if (responseState === 'Cancelled') return 'Cancelled'; - - if (responseConfirmed && confirmedInverseResponseSide) return 'Rejected'; - - if (responseState === 'Active' && !rfqExpired && !responseConfirmed) - return 'Approve'; - - if ( - confirmedResponseSide && - (responseState === 'SettlingPreparations' || - responseState === 'ReadyForSettling') && - !defaulted - ) - return 'Settle'; - - if (defaulted || responseState === 'Defaulted') return 'Defaulted'; - - if (responseState === 'Settled') return 'Settled'; - - return null; + if (responseConfirmed && confirmedResponseSide && defaulted) + return 'Defaulted'; + switch (caller) { + case 'maker': + switch (responseState) { + case 'Active': + if (!responseConfirmed) return 'Cancel'; + if (responseConfirmed && confirmedInverseResponseSide) + return 'Rejected'; + break; + case 'Expired': + if (!responseConfirmed && response.makerCollateralLocked > 0) + return 'UnlockCollateral'; + case 'Cancelled': + if (response.makerCollateralLocked > 0) return 'UnlockCollateral'; + if (response.makerCollateralLocked === 0) return 'Cleanup'; + break; + case 'SettlingPreparations': + case 'OnlyMakerPrepared': + case 'OnlyTakerPrepared': + case 'ReadyForSettling': + if (defaulted) return 'Defaulted'; + if (confirmedInverseResponseSide) return 'Rejected'; + if (confirmedResponseSide) return 'Settle'; + break; + case 'Settled': + if (confirmedInverseResponseSide) return 'Rejected'; + return 'Settled'; + case 'Defaulted': + if (confirmedInverseResponseSide) return 'Rejected'; + return 'Defaulted'; + } + break; + + case 'taker': + switch (responseState) { + case 'Active': + if (!responseConfirmed) return 'Approve'; + if (responseConfirmed && confirmedInverseResponseSide) + return 'Rejected'; + break; + case 'Expired': + return 'Expired'; + case 'Cancelled': + return 'Cancelled'; + case 'SettlingPreparations': + case 'OnlyMakerPrepared': + case 'OnlyTakerPrepared': + case 'ReadyForSettling': + if (confirmedInverseResponseSide) return 'Rejected'; + if (confirmedResponseSide) return 'Settle'; + break; + case 'Settled': + if (confirmedInverseResponseSide) return 'Rejected'; + return 'Settled'; + case 'Defaulted': + if (confirmedInverseResponseSide) return 'Rejected'; + return 'Defaulted'; + } + break; } return null; }; diff --git a/packages/js/tests/unit/responseStateAndAction.spec.ts b/packages/js/tests/unit/responseStateAndAction.spec.ts index 2e2b145e6..3ac1e86ff 100644 --- a/packages/js/tests/unit/responseStateAndAction.spec.ts +++ b/packages/js/tests/unit/responseStateAndAction.spec.ts @@ -244,6 +244,8 @@ describe('unit.responseStateAndAction', () => { ], orderType: 'two-way', quoteAsset: await SpotQuoteInstrument.create(takerCvg, quoteMint), + activeWindow: 6, + settlingWindow: 3, fixedSize, }); const { rfqResponse } = await makerCvg.rfqs().respond({ @@ -266,7 +268,7 @@ describe('unit.responseStateAndAction', () => { address: rfqResponse.address, }); - //Approve for taker + //Settle for taker expect( takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, @@ -285,5 +287,33 @@ describe('unit.responseStateAndAction', () => { responseSide: 'ask', }).responseAction ).toBe('Rejected'); + + await takerCvg.rfqs().prepareSettlement({ + response: rfqResponse.address, + rfq: rfq.address, + legAmountToPrepare: rfq.legs.length, + }); + + await sleep(9); + + //Defaulted for taker + expect( + takerCvg.rfqs().getResponseStateAndAction({ + response: refreshedResponse, + rfq, + caller: 'taker', + responseSide: 'bid', + }).responseAction + ).toBe('Defaulted'); + + //Defaulted for maker + expect( + takerCvg.rfqs().getResponseStateAndAction({ + response: refreshedResponse, + rfq, + caller: 'maker', + responseSide: 'bid', + }).responseAction + ).toBe('Defaulted'); }); }); From aeb10855595d537785bce4714afeb374a38edb5e Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Wed, 6 Sep 2023 15:53:14 +0530 Subject: [PATCH 07/11] refactor ResponseState to have Rejected state --- .../operations/getResponseStateAndAction.ts | 106 +++++++----------- .../tests/unit/responseStateAndAction.spec.ts | 12 +- 2 files changed, 48 insertions(+), 70 deletions(-) diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts index bdf1379a8..fcc4ce00d 100644 --- a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts +++ b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts @@ -11,19 +11,15 @@ export type ResponseState = | 'ReadyForSettling' | 'WaitingForLastLook' | 'OnlyMakerPrepared' - | 'OnlyTakerPrepared'; + | 'OnlyTakerPrepared' + | 'Rejected'; export type ResponseAction = | 'Cancel' | 'UnlockCollateral' | 'Cleanup' - | 'Rejected' - | 'Defaulted' | 'Settle' - | 'Settled' - | 'Expired' | 'Approve' - | 'Cancelled' | null; /** @@ -98,15 +94,15 @@ export const getResponseStateAndActionHandler: SyncOperationHandler { const rfqExpired = timestampExpiry.getTime() <= Date.now(); const settlementWindowElapsed = timestampSettlement.getTime() <= Date.now(); + const confirmedInverseResponseSide = + (responseSide === 'ask' && response?.confirmed?.side !== 'ask') || + (responseSide === 'bid' && response?.confirmed?.side !== 'bid'); + const responseConfirmed = response?.confirmed !== null; + + const takerPreparedLegsComplete = + response.takerPreparedLegs === rfq.legs.length; + const makerPreparedLegsComplete = + response.makerPreparedLegs === rfq.legs.length; + const partyPreparedLegsComplete = + caller === 'maker' ? makerPreparedLegsComplete : takerPreparedLegsComplete; + const counterpartyPreparedLegsComplete = + caller === 'maker' ? takerPreparedLegsComplete : makerPreparedLegsComplete; + const defaulted = response.defaultingParty + ? response.defaultingParty !== null + : settlementWindowElapsed && + (!partyPreparedLegsComplete || !counterpartyPreparedLegsComplete); + + if (responseConfirmed && confirmedInverseResponseSide) return 'Rejected'; + switch (response.state) { case 'active': if (!rfqExpired) return 'Active'; @@ -130,6 +148,7 @@ const getResponseState = ( if (!rfqExpired) return 'WaitingForLastLook'; return 'Expired'; case 'settling-preparations': + if (defaulted) return 'Defaulted'; if (!settlementWindowElapsed) { if (response.makerPreparedLegs === rfq.legs.length) return 'OnlyMakerPrepared'; @@ -139,6 +158,7 @@ const getResponseState = ( } return 'Defaulted'; case 'ready-for-settling': + if (defaulted) return 'Defaulted'; return 'ReadyForSettling'; case 'settled': return 'Settled'; @@ -151,45 +171,16 @@ const getResponseState = ( const getResponseAction = ( response: Response, - rfq: Rfq, - responseSide: 'ask' | 'bid', - caller: 'maker' | 'taker', responseState: ResponseState, - timestampSettlement: Date + rfq: Rfq, + caller: 'maker' | 'taker' ): ResponseAction => { - const confirmedResponseSide = - (responseSide === 'ask' && response?.confirmed?.side === 'ask') || - (responseSide === 'bid' && response?.confirmed?.side === 'bid'); - - const confirmedInverseResponseSide = - (responseSide === 'ask' && response?.confirmed?.side !== 'ask') || - (responseSide === 'bid' && response?.confirmed?.side !== 'bid'); - - const takerPreparedLegsComplete = - response.takerPreparedLegs === rfq.legs.length; - const makerPreparedLegsComplete = - response.makerPreparedLegs === rfq.legs.length; - const partyPreparedLegsComplete = - caller === 'maker' ? makerPreparedLegsComplete : takerPreparedLegsComplete; - const counterpartyPreparedLegsComplete = - caller === 'maker' ? takerPreparedLegsComplete : makerPreparedLegsComplete; - - const settlementWindowElapsed = timestampSettlement.getTime() <= Date.now(); - const defaulted = response.defaultingParty - ? response.defaultingParty !== null - : settlementWindowElapsed && - (!partyPreparedLegsComplete || !counterpartyPreparedLegsComplete); - const responseConfirmed = response?.confirmed !== null; - if (responseConfirmed && confirmedResponseSide && defaulted) - return 'Defaulted'; switch (caller) { case 'maker': switch (responseState) { case 'Active': if (!responseConfirmed) return 'Cancel'; - if (responseConfirmed && confirmedInverseResponseSide) - return 'Rejected'; break; case 'Expired': if (!responseConfirmed && response.makerCollateralLocked > 0) @@ -202,16 +193,11 @@ const getResponseAction = ( case 'OnlyMakerPrepared': case 'OnlyTakerPrepared': case 'ReadyForSettling': - if (defaulted) return 'Defaulted'; - if (confirmedInverseResponseSide) return 'Rejected'; - if (confirmedResponseSide) return 'Settle'; - break; + return 'Settle'; case 'Settled': - if (confirmedInverseResponseSide) return 'Rejected'; - return 'Settled'; case 'Defaulted': - if (confirmedInverseResponseSide) return 'Rejected'; - return 'Defaulted'; + case 'Rejected': + return null; } break; @@ -219,26 +205,18 @@ const getResponseAction = ( switch (responseState) { case 'Active': if (!responseConfirmed) return 'Approve'; - if (responseConfirmed && confirmedInverseResponseSide) - return 'Rejected'; break; - case 'Expired': - return 'Expired'; - case 'Cancelled': - return 'Cancelled'; case 'SettlingPreparations': case 'OnlyMakerPrepared': case 'OnlyTakerPrepared': case 'ReadyForSettling': - if (confirmedInverseResponseSide) return 'Rejected'; - if (confirmedResponseSide) return 'Settle'; - break; + return 'Settle'; case 'Settled': - if (confirmedInverseResponseSide) return 'Rejected'; - return 'Settled'; case 'Defaulted': - if (confirmedInverseResponseSide) return 'Rejected'; - return 'Defaulted'; + case 'Expired': + case 'Cancelled': + case 'Rejected': + return null; } break; } diff --git a/packages/js/tests/unit/responseStateAndAction.spec.ts b/packages/js/tests/unit/responseStateAndAction.spec.ts index 3ac1e86ff..335cde448 100644 --- a/packages/js/tests/unit/responseStateAndAction.spec.ts +++ b/packages/js/tests/unit/responseStateAndAction.spec.ts @@ -68,7 +68,7 @@ describe('unit.responseStateAndAction', () => { rfq, caller: 'taker', responseSide: 'bid', - }).responseAction + }).responseState ).toBe('Cancelled'); //Unlock Response Collateral for maker @@ -195,7 +195,7 @@ describe('unit.responseStateAndAction', () => { rfq, caller: 'maker', responseSide: 'bid', - }).responseAction + }).responseState ).toBe('Settled'); }); @@ -229,7 +229,7 @@ describe('unit.responseStateAndAction', () => { rfq, caller: 'taker', responseSide: 'bid', - }).responseAction + }).responseState ).toBe('Expired'); }); @@ -285,7 +285,7 @@ describe('unit.responseStateAndAction', () => { rfq, caller: 'taker', responseSide: 'ask', - }).responseAction + }).responseState ).toBe('Rejected'); await takerCvg.rfqs().prepareSettlement({ @@ -303,7 +303,7 @@ describe('unit.responseStateAndAction', () => { rfq, caller: 'taker', responseSide: 'bid', - }).responseAction + }).responseState ).toBe('Defaulted'); //Defaulted for maker @@ -313,7 +313,7 @@ describe('unit.responseStateAndAction', () => { rfq, caller: 'maker', responseSide: 'bid', - }).responseAction + }).responseState ).toBe('Defaulted'); }); }); From aea250e1a0190920dd93e21aa622e34f7d0aa842 Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Thu, 7 Sep 2023 10:25:15 +0530 Subject: [PATCH 08/11] add more states --- .../operations/getResponseStateAndAction.ts | 57 ++++++++++++------- .../tests/unit/responseStateAndAction.spec.ts | 6 +- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts index fcc4ce00d..71ad70c44 100644 --- a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts +++ b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts @@ -1,3 +1,4 @@ +import { DefaultingParty } from '@convergence-rfq/rfq'; import { Rfq, Response } from '../models'; import { Operation, SyncOperationHandler, useOperation } from '../../../types'; const Key = 'GetResponseStateAndAction' as const; @@ -5,7 +6,9 @@ export type ResponseState = | 'Active' | 'Cancelled' | 'Expired' - | 'Defaulted' + | 'MakerDefaulted' + | 'TakerDefaulted' + | 'BothDefaulted' | 'Settled' | 'SettlingPreparations' | 'ReadyForSettling' @@ -20,6 +23,8 @@ export type ResponseAction = | 'Cleanup' | 'Settle' | 'Approve' + | 'Settle One Party Defaulted' + | 'Settle Both Parties Defaulted' | null; /** @@ -123,21 +128,8 @@ const getResponseState = ( (responseSide === 'bid' && response?.confirmed?.side !== 'bid'); const responseConfirmed = response?.confirmed !== null; - const takerPreparedLegsComplete = - response.takerPreparedLegs === rfq.legs.length; - const makerPreparedLegsComplete = - response.makerPreparedLegs === rfq.legs.length; - const partyPreparedLegsComplete = - caller === 'maker' ? makerPreparedLegsComplete : takerPreparedLegsComplete; - const counterpartyPreparedLegsComplete = - caller === 'maker' ? takerPreparedLegsComplete : makerPreparedLegsComplete; - const defaulted = response.defaultingParty - ? response.defaultingParty !== null - : settlementWindowElapsed && - (!partyPreparedLegsComplete || !counterpartyPreparedLegsComplete); - if (responseConfirmed && confirmedInverseResponseSide) return 'Rejected'; - + const { defaultingParty } = response; switch (response.state) { case 'active': if (!rfqExpired) return 'Active'; @@ -148,7 +140,6 @@ const getResponseState = ( if (!rfqExpired) return 'WaitingForLastLook'; return 'Expired'; case 'settling-preparations': - if (defaulted) return 'Defaulted'; if (!settlementWindowElapsed) { if (response.makerPreparedLegs === rfq.legs.length) return 'OnlyMakerPrepared'; @@ -156,14 +147,27 @@ const getResponseState = ( return 'OnlyTakerPrepared'; return 'SettlingPreparations'; } - return 'Defaulted'; + switch (defaultingParty) { + case DefaultingParty.Maker: + return 'MakerDefaulted'; + case DefaultingParty.Taker: + return 'TakerDefaulted'; + case DefaultingParty.Both: + return 'BothDefaulted'; + } case 'ready-for-settling': - if (defaulted) return 'Defaulted'; return 'ReadyForSettling'; case 'settled': return 'Settled'; case 'defaulted': - return 'Defaulted'; + switch (defaultingParty) { + case DefaultingParty.Maker: + return 'MakerDefaulted'; + case DefaultingParty.Taker: + return 'TakerDefaulted'; + case DefaultingParty.Both: + return 'BothDefaulted'; + } default: throw new Error('Invalid Response state'); } @@ -195,7 +199,13 @@ const getResponseAction = ( case 'ReadyForSettling': return 'Settle'; case 'Settled': - case 'Defaulted': + if (response.makerCollateralLocked > 0) return 'UnlockCollateral'; + if (response.makerCollateralLocked === 0) return 'Cleanup'; + case 'MakerDefaulted': + case 'TakerDefaulted': + return 'Settle One Party Defaulted'; + case 'BothDefaulted': + return 'Settle Both Parties Defaulted'; case 'Rejected': return null; } @@ -212,7 +222,12 @@ const getResponseAction = ( case 'ReadyForSettling': return 'Settle'; case 'Settled': - case 'Defaulted': + case 'MakerDefaulted': + return 'Settle One Party Defaulted'; + case 'TakerDefaulted': + return 'Settle One Party Defaulted'; + case 'BothDefaulted': + return 'Settle Both Parties Defaulted'; case 'Expired': case 'Cancelled': case 'Rejected': diff --git a/packages/js/tests/unit/responseStateAndAction.spec.ts b/packages/js/tests/unit/responseStateAndAction.spec.ts index 335cde448..c2f7f7ce7 100644 --- a/packages/js/tests/unit/responseStateAndAction.spec.ts +++ b/packages/js/tests/unit/responseStateAndAction.spec.ts @@ -244,8 +244,8 @@ describe('unit.responseStateAndAction', () => { ], orderType: 'two-way', quoteAsset: await SpotQuoteInstrument.create(takerCvg, quoteMint), - activeWindow: 6, - settlingWindow: 3, + activeWindow: 2, + settlingWindow: 1, fixedSize, }); const { rfqResponse } = await makerCvg.rfqs().respond({ @@ -294,7 +294,7 @@ describe('unit.responseStateAndAction', () => { legAmountToPrepare: rfq.legs.length, }); - await sleep(9); + await sleep(3); //Defaulted for taker expect( From 2bf43abb05a21755228b8b0a3cb666011f1ec8f7 Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Fri, 8 Sep 2023 11:00:04 +0530 Subject: [PATCH 09/11] fix defaulted state --- .../operations/getResponseStateAndAction.ts | 28 +++++++++++++++---- .../tests/unit/responseStateAndAction.spec.ts | 15 ++++++---- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts index 71ad70c44..550d8464b 100644 --- a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts +++ b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts @@ -24,7 +24,7 @@ export type ResponseAction = | 'Settle' | 'Approve' | 'Settle One Party Defaulted' - | 'Settle Both Parties Defaulted' + | 'Settle Both Party Defaulted' | null; /** @@ -127,9 +127,25 @@ const getResponseState = ( (responseSide === 'ask' && response?.confirmed?.side !== 'ask') || (responseSide === 'bid' && response?.confirmed?.side !== 'bid'); const responseConfirmed = response?.confirmed !== null; + const takerPreparedLegsComplete = + response.takerPreparedLegs === rfq.legs.length; + const makerPreparedLegsComplete = + response.makerPreparedLegs === rfq.legs.length; + const defaulted = response.defaultingParty + ? response.defaultingParty !== null + : settlementWindowElapsed && + (!makerPreparedLegsComplete || !takerPreparedLegsComplete); + + let { defaultingParty } = response; + + if (defaulted && defaultingParty === null) { + if (!makerPreparedLegsComplete && !takerPreparedLegsComplete) + defaultingParty = DefaultingParty.Both; + if (!makerPreparedLegsComplete) defaultingParty = DefaultingParty.Maker; + if (!takerPreparedLegsComplete) defaultingParty = DefaultingParty.Taker; + } if (responseConfirmed && confirmedInverseResponseSide) return 'Rejected'; - const { defaultingParty } = response; switch (response.state) { case 'active': if (!rfqExpired) return 'Active'; @@ -187,8 +203,6 @@ const getResponseAction = ( if (!responseConfirmed) return 'Cancel'; break; case 'Expired': - if (!responseConfirmed && response.makerCollateralLocked > 0) - return 'UnlockCollateral'; case 'Cancelled': if (response.makerCollateralLocked > 0) return 'UnlockCollateral'; if (response.makerCollateralLocked === 0) return 'Cleanup'; @@ -205,7 +219,7 @@ const getResponseAction = ( case 'TakerDefaulted': return 'Settle One Party Defaulted'; case 'BothDefaulted': - return 'Settle Both Parties Defaulted'; + return 'Settle Both Party Defaulted'; case 'Rejected': return null; } @@ -227,9 +241,11 @@ const getResponseAction = ( case 'TakerDefaulted': return 'Settle One Party Defaulted'; case 'BothDefaulted': - return 'Settle Both Parties Defaulted'; + return 'Settle Both Party Defaulted'; case 'Expired': case 'Cancelled': + if (response.takerCollateralLocked > 0) return 'UnlockCollateral'; + if (response.takerCollateralLocked === 0) return 'Cleanup'; case 'Rejected': return null; } diff --git a/packages/js/tests/unit/responseStateAndAction.spec.ts b/packages/js/tests/unit/responseStateAndAction.spec.ts index c2f7f7ce7..a492b9c13 100644 --- a/packages/js/tests/unit/responseStateAndAction.spec.ts +++ b/packages/js/tests/unit/responseStateAndAction.spec.ts @@ -234,6 +234,7 @@ describe('unit.responseStateAndAction', () => { }); it('[Rejected, Defaulted]', async () => { + let refreshedResponse: Response; const fixedSize: FixedSize = { type: 'fixed-base', amount: 19.653_038_331, @@ -264,7 +265,7 @@ describe('unit.responseStateAndAction', () => { rfq: rfq.address, }); - const refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ + refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ address: rfqResponse.address, }); @@ -296,7 +297,10 @@ describe('unit.responseStateAndAction', () => { await sleep(3); - //Defaulted for taker + refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ + address: rfqResponse.address, + }); + //MkaerDefaulted for taker expect( takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, @@ -304,9 +308,8 @@ describe('unit.responseStateAndAction', () => { caller: 'taker', responseSide: 'bid', }).responseState - ).toBe('Defaulted'); - - //Defaulted for maker + ).toBe('MakerDefaulted'); + //MakerDefaulted for maker expect( takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, @@ -314,6 +317,6 @@ describe('unit.responseStateAndAction', () => { caller: 'maker', responseSide: 'bid', }).responseState - ).toBe('Defaulted'); + ).toBe('MakerDefaulted'); }); }); From a5a7f52342225ac4b9b360ce6b9591822aa9651f Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Fri, 8 Sep 2023 13:49:17 +0530 Subject: [PATCH 10/11] simplify fetching defaulting party logic --- .../plugins/rfqModule/models/ResponseSide.ts | 7 ++ .../operations/getResponseStateAndAction.ts | 92 +++++++++++-------- .../tests/unit/responseStateAndAction.spec.ts | 2 +- 3 files changed, 60 insertions(+), 41 deletions(-) diff --git a/packages/js/src/plugins/rfqModule/models/ResponseSide.ts b/packages/js/src/plugins/rfqModule/models/ResponseSide.ts index 670953c16..40f7c99e2 100644 --- a/packages/js/src/plugins/rfqModule/models/ResponseSide.ts +++ b/packages/js/src/plugins/rfqModule/models/ResponseSide.ts @@ -28,3 +28,10 @@ export function toSolitaQuoteSide(responseSide: ResponseSide): SolitaQuoteSide { } } } + +export const inverseResponseSide = (side: ResponseSide): ResponseSide => { + if (side === Bid) { + return Ask; + } + return Bid; +}; diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts index 550d8464b..c3dc64df4 100644 --- a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts +++ b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts @@ -1,5 +1,11 @@ import { DefaultingParty } from '@convergence-rfq/rfq'; -import { Rfq, Response } from '../models'; +import { + Rfq, + Response, + ResponseSide, + AuthoritySide, + inverseResponseSide, +} from '../models'; import { Operation, SyncOperationHandler, useOperation } from '../../../types'; const Key = 'GetResponseStateAndAction' as const; export type ResponseState = @@ -100,15 +106,9 @@ export const getResponseStateAndActionHandler: SyncOperationHandler { const rfqExpired = timestampExpiry.getTime() <= Date.now(); const settlementWindowElapsed = timestampSettlement.getTime() <= Date.now(); const confirmedInverseResponseSide = - (responseSide === 'ask' && response?.confirmed?.side !== 'ask') || - (responseSide === 'bid' && response?.confirmed?.side !== 'bid'); + response?.confirmed?.side === inverseResponseSide(responseSide); const responseConfirmed = response?.confirmed !== null; - const takerPreparedLegsComplete = - response.takerPreparedLegs === rfq.legs.length; - const makerPreparedLegsComplete = - response.makerPreparedLegs === rfq.legs.length; - const defaulted = response.defaultingParty - ? response.defaultingParty !== null - : settlementWindowElapsed && - (!makerPreparedLegsComplete || !takerPreparedLegsComplete); - - let { defaultingParty } = response; - - if (defaulted && defaultingParty === null) { - if (!makerPreparedLegsComplete && !takerPreparedLegsComplete) - defaultingParty = DefaultingParty.Both; - if (!makerPreparedLegsComplete) defaultingParty = DefaultingParty.Maker; - if (!takerPreparedLegsComplete) defaultingParty = DefaultingParty.Taker; - } + const makerPrepared = hasMakerPrepared(response, rfq); + const takerPrepared = hasTakerPrepared(response, rfq); + const defaultingParty = getDefautingParty( + response, + settlementWindowElapsed, + makerPrepared, + takerPrepared + ); if (responseConfirmed && confirmedInverseResponseSide) return 'Rejected'; switch (response.state) { @@ -157,10 +146,8 @@ const getResponseState = ( return 'Expired'; case 'settling-preparations': if (!settlementWindowElapsed) { - if (response.makerPreparedLegs === rfq.legs.length) - return 'OnlyMakerPrepared'; - if (response.takerPreparedLegs === rfq.legs.length) - return 'OnlyTakerPrepared'; + if (makerPrepared) return 'OnlyMakerPrepared'; + if (takerPrepared) return 'OnlyTakerPrepared'; return 'SettlingPreparations'; } switch (defaultingParty) { @@ -192,8 +179,7 @@ const getResponseState = ( const getResponseAction = ( response: Response, responseState: ResponseState, - rfq: Rfq, - caller: 'maker' | 'taker' + caller: AuthoritySide ): ResponseAction => { const responseConfirmed = response?.confirmed !== null; switch (caller) { @@ -204,17 +190,14 @@ const getResponseAction = ( break; case 'Expired': case 'Cancelled': + case 'Settled': if (response.makerCollateralLocked > 0) return 'UnlockCollateral'; if (response.makerCollateralLocked === 0) return 'Cleanup'; - break; case 'SettlingPreparations': case 'OnlyMakerPrepared': case 'OnlyTakerPrepared': case 'ReadyForSettling': return 'Settle'; - case 'Settled': - if (response.makerCollateralLocked > 0) return 'UnlockCollateral'; - if (response.makerCollateralLocked === 0) return 'Cleanup'; case 'MakerDefaulted': case 'TakerDefaulted': return 'Settle One Party Defaulted'; @@ -235,13 +218,13 @@ const getResponseAction = ( case 'OnlyTakerPrepared': case 'ReadyForSettling': return 'Settle'; - case 'Settled': case 'MakerDefaulted': return 'Settle One Party Defaulted'; case 'TakerDefaulted': return 'Settle One Party Defaulted'; case 'BothDefaulted': return 'Settle Both Party Defaulted'; + case 'Settled': case 'Expired': case 'Cancelled': if (response.takerCollateralLocked > 0) return 'UnlockCollateral'; @@ -253,3 +236,32 @@ const getResponseAction = ( } return null; }; + +const getDefautingParty = ( + response: Response, + settlementWindowElapsed: boolean, + makerPrepared: boolean, + takerPrepared: boolean +): DefaultingParty | null => { + let { defaultingParty } = response; + if (defaultingParty) return defaultingParty; + const defaulted = + settlementWindowElapsed && (!makerPrepared || !takerPrepared); + + if (defaulted && defaultingParty === null) { + if (!makerPrepared && !takerPrepared) + defaultingParty = DefaultingParty.Both; + if (!makerPrepared) defaultingParty = DefaultingParty.Maker; + if (!takerPrepared) defaultingParty = DefaultingParty.Taker; + } + + return null; +}; + +const hasMakerPrepared = (response: Response, rfq: Rfq) => { + return response.makerPreparedLegs === rfq.legs.length; +}; + +const hasTakerPrepared = (response: Response, rfq: Rfq) => { + return response.takerPreparedLegs === rfq.legs.length; +}; diff --git a/packages/js/tests/unit/responseStateAndAction.spec.ts b/packages/js/tests/unit/responseStateAndAction.spec.ts index a492b9c13..81dcc7c0e 100644 --- a/packages/js/tests/unit/responseStateAndAction.spec.ts +++ b/packages/js/tests/unit/responseStateAndAction.spec.ts @@ -300,7 +300,7 @@ describe('unit.responseStateAndAction', () => { refreshedResponse = await makerCvg.rfqs().findResponseByAddress({ address: rfqResponse.address, }); - //MkaerDefaulted for taker + //MakerDefaulted for taker expect( takerCvg.rfqs().getResponseStateAndAction({ response: refreshedResponse, From ebb04155954e56fde7b3a912c310f364073c2c35 Mon Sep 17 00:00:00 2001 From: Nagaprasadvr Date: Fri, 8 Sep 2023 14:18:02 +0530 Subject: [PATCH 11/11] add RfqTimers class --- .../operations/getResponseStateAndAction.ts | 28 +++++++------------ packages/js/src/utils/classes.ts | 25 +++++++++++++++++ 2 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 packages/js/src/utils/classes.ts diff --git a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts index c3dc64df4..6e5c630b6 100644 --- a/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts +++ b/packages/js/src/plugins/rfqModule/operations/getResponseStateAndAction.ts @@ -7,6 +7,7 @@ import { inverseResponseSide, } from '../models'; import { Operation, SyncOperationHandler, useOperation } from '../../../types'; +import { RfqTimers } from '@/utils/classes'; const Key = 'GetResponseStateAndAction' as const; export type ResponseState = | 'Active' @@ -94,18 +95,11 @@ export const getResponseStateAndActionHandler: SyncOperationHandler { - const rfqExpired = timestampExpiry.getTime() <= Date.now(); - const settlementWindowElapsed = timestampSettlement.getTime() <= Date.now(); + const rfqExpired = rfqTimers.isRfqExpired(); + const settlementWindowElapsed = rfqTimers.isRfqSettlementWindowElapsed(); const confirmedInverseResponseSide = response?.confirmed?.side === inverseResponseSide(responseSide); const responseConfirmed = response?.confirmed !== null; @@ -243,16 +236,15 @@ const getDefautingParty = ( makerPrepared: boolean, takerPrepared: boolean ): DefaultingParty | null => { - let { defaultingParty } = response; + const { defaultingParty } = response; if (defaultingParty) return defaultingParty; const defaulted = settlementWindowElapsed && (!makerPrepared || !takerPrepared); if (defaulted && defaultingParty === null) { - if (!makerPrepared && !takerPrepared) - defaultingParty = DefaultingParty.Both; - if (!makerPrepared) defaultingParty = DefaultingParty.Maker; - if (!takerPrepared) defaultingParty = DefaultingParty.Taker; + if (!makerPrepared && !takerPrepared) return DefaultingParty.Both; + if (!makerPrepared) return DefaultingParty.Maker; + if (!takerPrepared) return DefaultingParty.Taker; } return null; diff --git a/packages/js/src/utils/classes.ts b/packages/js/src/utils/classes.ts new file mode 100644 index 000000000..0c8af3042 --- /dev/null +++ b/packages/js/src/utils/classes.ts @@ -0,0 +1,25 @@ +import { Rfq } from '@/plugins/rfqModule'; + +export class RfqTimers { + public timestampExpiry: Date; + public timestampStart: Date; + public timeStampSettlement: Date; + + constructor(rfq: Rfq) { + this.timestampStart = new Date(Number(rfq.creationTimestamp)); + this.timestampExpiry = new Date( + this.timestampStart.getTime() + Number(rfq.activeWindow) * 1000 + ); + this.timeStampSettlement = new Date( + this.timestampExpiry.getTime() + Number(rfq.settlingWindow) * 1000 + ); + } + + isRfqExpired(): boolean { + return Date.now() >= this.timestampExpiry.getTime(); + } + + isRfqSettlementWindowElapsed(): boolean { + return Date.now() >= this.timeStampSettlement.getTime(); + } +}