Skip to content

Commit b65b9ca

Browse files
authored
add support for update_dependency in trigger-client (#430)
1 parent 8303e55 commit b65b9ca

11 files changed

Lines changed: 308 additions & 1 deletion

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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+
);

packages/engine-client/src/types/serverExecuteTypes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
EIP712OrderValues,
99
EIP712ProductOrdersCancellationValues,
1010
EIP712TransferQuoteValues,
11+
EIP712UpdateDependencyValues,
1112
EIP712WithdrawCollateralV2Values,
1213
EIP712WithdrawCollateralValues,
1314
SignedTx,
@@ -25,6 +26,10 @@ export interface EngineServerCancelOrdersResponse {
2526
cancelled_orders: EngineServerOrderResponse[];
2627
}
2728

29+
export interface EngineServerUpdateDependencyResponse {
30+
digest: string;
31+
}
32+
2833
export interface EngineServerExecuteResponseDataByType {
2934
burn_nlp: null;
3035
cancel_and_place: EngineServerPlaceOrderResponse;
@@ -36,6 +41,7 @@ export interface EngineServerExecuteResponseDataByType {
3641
place_order: EngineServerPlaceOrderResponse;
3742
place_orders: EngineServerPlaceOrdersResponse;
3843
transfer_quote: null;
44+
update_dependency: EngineServerUpdateDependencyResponse;
3945
withdraw_collateral: null;
4046
withdraw_collateral_v2: null;
4147
}
@@ -125,6 +131,7 @@ export interface EngineServerExecuteRequestByType {
125131
stop_on_failure: boolean | null;
126132
};
127133
transfer_quote: SignedTx<EIP712TransferQuoteValues>;
134+
update_dependency: SignedTx<EIP712UpdateDependencyValues>;
128135
withdraw_collateral: WithSpotLeverage<
129136
SignedTx<EIP712WithdrawCollateralValues>
130137
>;

packages/shared/src/eip712/eip712ValueTypes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
EIP712OrderParams,
1313
EIP712SocialAuthenticationParams,
1414
EIP712TransferQuoteParams,
15+
EIP712UpdateDependencyParams,
1516
EIP712WithdrawCollateralParams,
1617
EIP712WithdrawCollateralV2Params,
1718
} from './signatureParamTypes';
@@ -47,6 +48,9 @@ export type EIP712OrderValues = Omit<
4748
export type EIP712ListTriggerOrdersValues =
4849
WithEIP712Sender<EIP712ListTriggerOrdersParams>;
4950

51+
export type EIP712UpdateDependencyValues =
52+
WithEIP712Sender<EIP712UpdateDependencyParams>;
53+
5054
export type EIP712OrderCancellationValues =
5155
WithEIP712Sender<EIP712CancelOrdersParams>;
5256

@@ -88,6 +92,7 @@ export interface SignableRequestTypeToEIP712Values {
8892
place_order: EIP712OrderValues;
8993
social_authentication: EIP712SocialAuthenticationValues;
9094
transfer_quote: EIP712TransferQuoteValues;
95+
update_dependency: EIP712UpdateDependencyValues;
9196
withdraw_collateral: EIP712WithdrawCollateralValues;
9297
withdraw_collateral_v2: EIP712WithdrawCollateralV2Values;
9398
}

packages/shared/src/eip712/getNadoEIP712Types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ export function getNadoEIP712Types(
5252
{ name: 'recvTime', type: 'uint64' },
5353
],
5454
};
55+
case 'update_dependency':
56+
return {
57+
DependencyUpdate: [
58+
{ name: 'sender', type: 'bytes32' },
59+
{ name: 'oldDigest', type: 'bytes32' },
60+
{ name: 'newDigest', type: 'bytes32' },
61+
{ name: 'nonce', type: 'uint64' },
62+
],
63+
};
5564
case 'cancel_orders':
5665
return {
5766
Cancellation: [

packages/shared/src/eip712/getNadoEIP712Values.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
EIP712ProductOrdersCancellationValues,
1313
EIP712SocialAuthenticationValues,
1414
EIP712TransferQuoteValues,
15+
EIP712UpdateDependencyValues,
1516
EIP712WithdrawCollateralV2Values,
1617
EIP712WithdrawCollateralValues,
1718
SignableRequestTypeToEIP712Values,
@@ -32,6 +33,7 @@ import {
3233
EIP712OrderParams,
3334
EIP712SocialAuthenticationParams,
3435
EIP712TransferQuoteParams,
36+
EIP712UpdateDependencyParams,
3537
EIP712WithdrawCollateralParams,
3638
EIP712WithdrawCollateralV2Params,
3739
} from './signatureParamTypes';
@@ -69,6 +71,11 @@ export function getNadoEIP712Values<TReqType extends SignableRequestType>(
6971
params as EIP712ListTriggerOrdersParams,
7072
);
7173
break;
74+
case 'update_dependency':
75+
values = getUpdateDependencyValues(
76+
params as EIP712UpdateDependencyParams,
77+
);
78+
break;
7279
case 'cancel_orders':
7380
values = getOrderCancellationValues(params as EIP712CancelOrdersParams);
7481
break;
@@ -167,6 +174,20 @@ function getListTriggerOrdersValues(
167174
};
168175
}
169176

177+
function getUpdateDependencyValues(
178+
params: EIP712UpdateDependencyParams,
179+
): EIP712UpdateDependencyValues {
180+
return {
181+
sender: subaccountToHex({
182+
subaccountOwner: params.subaccountOwner,
183+
subaccountName: params.subaccountName,
184+
}),
185+
oldDigest: params.oldDigest,
186+
newDigest: params.newDigest,
187+
nonce: params.nonce,
188+
};
189+
}
190+
170191
function getOrderCancellationValues(
171192
params: EIP712CancelOrdersParams,
172193
): EIP712OrderCancellationValues {

packages/shared/src/eip712/signableRequestType.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
EIP712OrderParams,
1111
EIP712SocialAuthenticationParams,
1212
EIP712TransferQuoteParams,
13+
EIP712UpdateDependencyParams,
1314
EIP712WithdrawCollateralParams,
1415
EIP712WithdrawCollateralV2Params,
1516
} from './signatureParamTypes';
@@ -29,6 +30,7 @@ export interface SignableRequestTypeToParams {
2930
place_order: EIP712OrderParams;
3031
social_authentication: EIP712SocialAuthenticationParams;
3132
transfer_quote: EIP712TransferQuoteParams;
33+
update_dependency: EIP712UpdateDependencyParams;
3234
withdraw_collateral: EIP712WithdrawCollateralParams;
3335
withdraw_collateral_v2: EIP712WithdrawCollateralV2Params;
3436
}

packages/shared/src/eip712/signatureParamTypes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ export interface EIP712ListTriggerOrdersParams extends Subaccount {
5454
recvTime: BigNumberish;
5555
}
5656

57+
export interface EIP712UpdateDependencyParams extends Subaccount {
58+
// Digest (bytes32 hex string) of the order the trigger orders currently depend on
59+
oldDigest: string;
60+
// Digest (bytes32 hex string) of the replacement order the trigger orders should depend on
61+
newDigest: string;
62+
nonce: string;
63+
}
64+
5765
export interface EIP712CancelOrdersParams extends Subaccount {
5866
productIds: number[];
5967
digests: string[];

packages/trigger-client/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ await trigger.cancelTriggerOrders({ ... });
4545

4646
### Executes
4747

48-
`placeTriggerOrder`, `placeTriggerOrders`, `cancelTriggerOrders`, `cancelProductOrders`.
48+
`placeTriggerOrder`, `placeTriggerOrders`, `cancelTriggerOrders`, `cancelProductOrders`, `updateTriggerDependency`.
4949

5050
### Queries
5151

0 commit comments

Comments
 (0)