|
| 1 | +import { |
| 2 | + EngineOrderParams, |
| 3 | + EnginePlaceOrderParams, |
| 4 | +} from '@nadohq/engine-client'; |
| 5 | +import { |
| 6 | + addDecimals, |
| 7 | + BigNumberish, |
| 8 | + getOrderNonce, |
| 9 | + getOrderVerifyingAddress, |
| 10 | + packOrderAppendix, |
| 11 | +} from '@nadohq/shared'; |
| 12 | +import { TriggerPlaceOrderParams } from '@nadohq/trigger-client'; |
| 13 | +import assert from 'node:assert/strict'; |
| 14 | +import { after, before, beforeEach, describe, test } from 'node:test'; |
| 15 | +import { |
| 16 | + assertArray, |
| 17 | + assertArrayElements, |
| 18 | + assertDefined, |
| 19 | + assertHexString, |
| 20 | +} from '../utils/assertions'; |
| 21 | +import { cleanupTestState } from '../utils/cleanup'; |
| 22 | +import { debugPrint } from '../utils/debugPrint'; |
| 23 | +import { delay } from '../utils/delay'; |
| 24 | +import { getExpiration } from '../utils/getExpiration'; |
| 25 | +import { createTestContext } from '../utils/runWithContext'; |
| 26 | +import { |
| 27 | + TEST_DELAYS, |
| 28 | + TEST_PRODUCT_IDS, |
| 29 | + TEST_SUBACCOUNT_NAME, |
| 30 | + TEST_TIMEOUTS, |
| 31 | +} from '../utils/testConstants'; |
| 32 | +import { RunContext } from '../utils/types'; |
| 33 | + |
| 34 | +void describe( |
| 35 | + '[trigger-client]: update dependency', |
| 36 | + { timeout: TEST_TIMEOUTS.DEFAULT }, |
| 37 | + () => { |
| 38 | + let tc: RunContext; |
| 39 | + |
| 40 | + // Digests captured during setup: `oldDigest` is the order the dependent |
| 41 | + // trigger initially depends on; `newDigest` is the replacement order. |
| 42 | + let oldDigest: string; |
| 43 | + let newDigest: string; |
| 44 | + // Dependent trigger order's digest, used to verify it stays in |
| 45 | + // `waiting_dependency` after the re-point. |
| 46 | + let dependentDigest: string; |
| 47 | + |
| 48 | + before(async () => { |
| 49 | + await delay(TEST_DELAYS.LONG); |
| 50 | + |
| 51 | + tc = createTestContext(); |
| 52 | + |
| 53 | + const marketPrice = await tc.engine.getMarketPrice({ |
| 54 | + productId: TEST_PRODUCT_IDS.SPOT_ETH, |
| 55 | + }); |
| 56 | + const midPrice = marketPrice.ask.plus(marketPrice.bid).div(2); |
| 57 | + const verifyingAddr = getOrderVerifyingAddress(TEST_PRODUCT_IDS.SPOT_ETH); |
| 58 | + |
| 59 | + const makeOrder = ( |
| 60 | + price: BigNumberish, |
| 61 | + orderKind: 'limit' | 'priceTriggered', |
| 62 | + ): EngineOrderParams => ({ |
| 63 | + amount: addDecimals(0.1), |
| 64 | + expiration: getExpiration(), |
| 65 | + price, |
| 66 | + subaccountName: TEST_SUBACCOUNT_NAME, |
| 67 | + subaccountOwner: tc.walletClientAddress, |
| 68 | + appendix: packOrderAppendix({ |
| 69 | + orderExecutionType: 'default', |
| 70 | + triggerType: orderKind === 'priceTriggered' ? 'price' : undefined, |
| 71 | + }), |
| 72 | + }); |
| 73 | + |
| 74 | + // Priced at 85% of mid (within the engine's 80%-120% oracle band) and |
| 75 | + // below the bid, so they rest on the book unfilled; the dependent |
| 76 | + // trigger order waits on these limit orders' fill before activating. |
| 77 | + // No `triggerType` in the appendix — these are regular limit orders. |
| 78 | + const dependencyOrderOld: EnginePlaceOrderParams = { |
| 79 | + productId: TEST_PRODUCT_IDS.SPOT_ETH, |
| 80 | + verifyingAddr, |
| 81 | + chainId: tc.chainId, |
| 82 | + spotLeverage: true, |
| 83 | + nonce: getOrderNonce(), |
| 84 | + id: 6000, |
| 85 | + order: makeOrder(midPrice.multipliedBy(0.85).dp(0), 'limit'), |
| 86 | + }; |
| 87 | + |
| 88 | + // Replacement dependency. After `update_dependency`, the dependent |
| 89 | + // trigger order should depend on this digest instead. |
| 90 | + const dependencyOrderNew: EnginePlaceOrderParams = { |
| 91 | + productId: TEST_PRODUCT_IDS.SPOT_ETH, |
| 92 | + verifyingAddr, |
| 93 | + chainId: tc.chainId, |
| 94 | + spotLeverage: true, |
| 95 | + nonce: getOrderNonce(), |
| 96 | + id: 6001, |
| 97 | + order: makeOrder(midPrice.multipliedBy(0.85).dp(0), 'limit'), |
| 98 | + }; |
| 99 | + |
| 100 | + const [dependencyOldResult, dependencyNewResult] = await Promise.all([ |
| 101 | + tc.engine.placeOrder(dependencyOrderOld), |
| 102 | + tc.engine.placeOrder(dependencyOrderNew), |
| 103 | + ]); |
| 104 | + |
| 105 | + oldDigest = dependencyOldResult.data.digest; |
| 106 | + newDigest = dependencyNewResult.data.digest; |
| 107 | + |
| 108 | + // Dependent trigger order that waits on `dependencyOrderOld`'s fill. |
| 109 | + const dependentTriggerOrder: TriggerPlaceOrderParams = { |
| 110 | + chainId: tc.chainId, |
| 111 | + order: makeOrder(midPrice.multipliedBy(1.05).dp(0), 'priceTriggered'), |
| 112 | + productId: TEST_PRODUCT_IDS.SPOT_ETH, |
| 113 | + spotLeverage: true, |
| 114 | + triggerCriteria: { |
| 115 | + type: 'price', |
| 116 | + criteria: { |
| 117 | + type: 'oracle_price_above', |
| 118 | + triggerPrice: midPrice.multipliedBy(1.1).dp(0), |
| 119 | + dependency: { |
| 120 | + digest: oldDigest, |
| 121 | + onPartialFill: false, |
| 122 | + }, |
| 123 | + }, |
| 124 | + }, |
| 125 | + verifyingAddr, |
| 126 | + nonce: getOrderNonce(), |
| 127 | + id: 6002, |
| 128 | + }; |
| 129 | + |
| 130 | + const dependentTriggerResult = await tc.trigger.placeTriggerOrder( |
| 131 | + dependentTriggerOrder, |
| 132 | + ); |
| 133 | + dependentDigest = dependentTriggerResult.data.digest; |
| 134 | + |
| 135 | + // Allow trigger service to index the placed orders. |
| 136 | + await delay(TEST_DELAYS.STANDARD); |
| 137 | + }); |
| 138 | + |
| 139 | + after(async () => { |
| 140 | + await cleanupTestState( |
| 141 | + { engine: tc.engine, trigger: tc.trigger }, |
| 142 | + { |
| 143 | + subaccountOwner: tc.walletClientAddress, |
| 144 | + endpointAddr: tc.endpointAddr, |
| 145 | + chainId: tc.chainId, |
| 146 | + }, |
| 147 | + ); |
| 148 | + }); |
| 149 | + |
| 150 | + beforeEach(async () => { |
| 151 | + await delay(TEST_DELAYS.STANDARD); |
| 152 | + }); |
| 153 | + |
| 154 | + void test('re-points dependent trigger orders to a new digest', async () => { |
| 155 | + const result = await tc.trigger.updateTriggerDependency({ |
| 156 | + oldDigest, |
| 157 | + newDigest, |
| 158 | + subaccountName: TEST_SUBACCOUNT_NAME, |
| 159 | + subaccountOwner: tc.walletClientAddress, |
| 160 | + verifyingAddr: tc.endpointAddr, |
| 161 | + chainId: tc.chainId, |
| 162 | + }); |
| 163 | + debugPrint('Update dependency result', result); |
| 164 | + |
| 165 | + assertDefined(result, 'updateDependencyResult'); |
| 166 | + assert.equal( |
| 167 | + result.status, |
| 168 | + 'success', |
| 169 | + 'update dependency should succeed', |
| 170 | + ); |
| 171 | + assertDefined(result.data, 'updateDependencyResult.data'); |
| 172 | + assertHexString(result.data.digest, 'updateDependencyResult.data.digest'); |
| 173 | + assert.equal( |
| 174 | + result.data.digest, |
| 175 | + newDigest, |
| 176 | + 'response digest should match the newDigest', |
| 177 | + ); |
| 178 | + assert.equal( |
| 179 | + result.request_type, |
| 180 | + 'execute_update_dependency', |
| 181 | + 'request_type should be execute_update_dependency', |
| 182 | + ); |
| 183 | + }); |
| 184 | + |
| 185 | + void test('dependent order remains in waiting_dependency after re-point', async () => { |
| 186 | + const listResult = await tc.trigger.listOrders({ |
| 187 | + chainId: tc.chainId, |
| 188 | + verifyingAddr: tc.endpointAddr, |
| 189 | + subaccountName: TEST_SUBACCOUNT_NAME, |
| 190 | + subaccountOwner: tc.walletClientAddress, |
| 191 | + digests: [dependentDigest], |
| 192 | + statusTypes: ['waiting_dependency'], |
| 193 | + }); |
| 194 | + debugPrint('Dependent order after re-point', listResult); |
| 195 | + |
| 196 | + assertDefined(listResult, 'listAfterRepointResult'); |
| 197 | + assertArray(listResult.orders, 'listAfterRepointResult.orders'); |
| 198 | + assert.equal( |
| 199 | + listResult.orders.length, |
| 200 | + 1, |
| 201 | + 'dependent order should still be listed in waiting_dependency', |
| 202 | + ); |
| 203 | + assertArrayElements( |
| 204 | + listResult.orders, |
| 205 | + (order) => { |
| 206 | + assert.equal( |
| 207 | + order.order.digest, |
| 208 | + dependentDigest, |
| 209 | + 'listed order digest should match the dependent order', |
| 210 | + ); |
| 211 | + assert.equal( |
| 212 | + order.status.type, |
| 213 | + 'waiting_dependency', |
| 214 | + 'dependent order status should still be waiting_dependency', |
| 215 | + ); |
| 216 | + }, |
| 217 | + 'listAfterRepointResult.orders', |
| 218 | + ); |
| 219 | + }); |
| 220 | + }, |
| 221 | +); |
0 commit comments