hyperquant 0.93__py3-none-any.whl → 0.94__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hyperquant/broker/auth.py +55 -0
- hyperquant/broker/bitmart.py +471 -0
- hyperquant/broker/lighter.py +470 -0
- hyperquant/broker/models/bitmart.py +487 -0
- hyperquant/broker/models/lighter.py +508 -0
- {hyperquant-0.93.dist-info → hyperquant-0.94.dist-info}/METADATA +2 -1
- {hyperquant-0.93.dist-info → hyperquant-0.94.dist-info}/RECORD +8 -4
- {hyperquant-0.93.dist-info → hyperquant-0.94.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Literal, Sequence
|
|
7
|
+
|
|
8
|
+
import pybotters
|
|
9
|
+
|
|
10
|
+
from lighter.api.account_api import AccountApi
|
|
11
|
+
from lighter.api.order_api import OrderApi
|
|
12
|
+
from lighter.api_client import ApiClient
|
|
13
|
+
from lighter.configuration import Configuration
|
|
14
|
+
from lighter.signer_client import SignerClient
|
|
15
|
+
|
|
16
|
+
from .models.lighter import LighterDataStore, _maybe_to_dict
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Lighter:
|
|
22
|
+
"""Lighter exchange client (REST + WebSocket) built on top of the official SDK."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
client: pybotters.Client,
|
|
27
|
+
*,
|
|
28
|
+
configuration: Configuration | None = None,
|
|
29
|
+
l1_address: str | None = None,
|
|
30
|
+
secret: str | None = None,
|
|
31
|
+
api_key_index: int = 3,
|
|
32
|
+
api_client: ApiClient | None = None,
|
|
33
|
+
order_api: OrderApi | None = None,
|
|
34
|
+
account_api: AccountApi | None = None,
|
|
35
|
+
ws_url: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.client = client
|
|
38
|
+
self.store = LighterDataStore()
|
|
39
|
+
self.l1_address = l1_address
|
|
40
|
+
self.account_index: int | None = None
|
|
41
|
+
self.secret:str = secret
|
|
42
|
+
self.api_key_index = api_key_index
|
|
43
|
+
|
|
44
|
+
self.configuration = configuration or Configuration.get_default()
|
|
45
|
+
self._api_client = api_client or ApiClient(configuration=self.configuration)
|
|
46
|
+
self._owns_api_client = api_client is None
|
|
47
|
+
|
|
48
|
+
self.order_api = order_api or OrderApi(self._api_client)
|
|
49
|
+
self.account_api = account_api or AccountApi(self._api_client)
|
|
50
|
+
self.signer: SignerClient = None
|
|
51
|
+
|
|
52
|
+
base_host = self.configuration.host.rstrip("/")
|
|
53
|
+
default_ws_url = f"{base_host.replace('https://', 'wss://')}/stream"
|
|
54
|
+
self.ws_url = ws_url or default_ws_url
|
|
55
|
+
self.id_to_symbol: dict[str, str] = {}
|
|
56
|
+
|
|
57
|
+
async def __aenter__(self) -> "Lighter":
|
|
58
|
+
await self.update("detail")
|
|
59
|
+
|
|
60
|
+
# 设置id_to_symbol映射
|
|
61
|
+
for detail in self.store.detail.find():
|
|
62
|
+
market_id = detail.get("market_id")
|
|
63
|
+
symbol = detail.get("symbol")
|
|
64
|
+
if market_id is not None and symbol is not None:
|
|
65
|
+
self.id_to_symbol[str(market_id)] = symbol
|
|
66
|
+
|
|
67
|
+
self.store.set_id_to_symbol(self.id_to_symbol)
|
|
68
|
+
|
|
69
|
+
# 尝试自动设置account_index
|
|
70
|
+
if self.l1_address is not None:
|
|
71
|
+
subact = await self.account_api.accounts_by_l1_address(
|
|
72
|
+
l1_address=self.l1_address
|
|
73
|
+
)
|
|
74
|
+
self.account_index = subact.sub_accounts[0].index
|
|
75
|
+
|
|
76
|
+
if self.secret:
|
|
77
|
+
|
|
78
|
+
self.signer = SignerClient(
|
|
79
|
+
url=self.configuration.host,
|
|
80
|
+
private_key=self.secret,
|
|
81
|
+
account_index=self.account_index if self.account_index is not None else -1,
|
|
82
|
+
api_key_index=self.api_key_index,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
88
|
+
await self.aclose()
|
|
89
|
+
|
|
90
|
+
async def aclose(self) -> None:
|
|
91
|
+
if self._owns_api_client:
|
|
92
|
+
await self._api_client.close()
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def auth(self):
|
|
96
|
+
if not self.signer:
|
|
97
|
+
raise RuntimeError("SignerClient is required for auth token generation")
|
|
98
|
+
auth, err = self.signer.create_auth_token_with_expiry(SignerClient.DEFAULT_10_MIN_AUTH_EXPIRY)
|
|
99
|
+
if err is not None:
|
|
100
|
+
raise Exception(err)
|
|
101
|
+
return auth
|
|
102
|
+
|
|
103
|
+
def get_contract_id(self, symbol: str) -> str | None:
|
|
104
|
+
"""Helper that resolves a symbol to its `market_id`."""
|
|
105
|
+
detail = self.store.detail.get({"symbol": symbol}) or self.store.detail.get({"market_id": symbol})
|
|
106
|
+
if not detail:
|
|
107
|
+
return None
|
|
108
|
+
market_id = detail.get("market_id")
|
|
109
|
+
if market_id is None and detail.get("order_book_index") is not None:
|
|
110
|
+
market_id = detail["order_book_index"]
|
|
111
|
+
return str(market_id) if market_id is not None else None
|
|
112
|
+
|
|
113
|
+
def _get_detail_entry(self, symbol: str | None = None, market_index: int | None = None) -> dict[str, Any] | None:
|
|
114
|
+
if symbol:
|
|
115
|
+
entry = self.store.detail.get({"symbol": symbol})
|
|
116
|
+
if entry:
|
|
117
|
+
return entry
|
|
118
|
+
|
|
119
|
+
if market_index is not None:
|
|
120
|
+
entries = self.store.detail.find({"market_id": market_index})
|
|
121
|
+
if entries:
|
|
122
|
+
return entries[0]
|
|
123
|
+
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
async def update(
|
|
127
|
+
self,
|
|
128
|
+
update_type: Literal[
|
|
129
|
+
"detail",
|
|
130
|
+
"orders",
|
|
131
|
+
"history_order",
|
|
132
|
+
"history_orders",
|
|
133
|
+
"account",
|
|
134
|
+
"positions",
|
|
135
|
+
"all",
|
|
136
|
+
] = "all",
|
|
137
|
+
*,
|
|
138
|
+
symbol: str | None = None,
|
|
139
|
+
limit: int = 50,
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Refresh cached data via Lighter REST endpoints."""
|
|
142
|
+
|
|
143
|
+
tasks: list[tuple[str, Any]] = []
|
|
144
|
+
|
|
145
|
+
include_detail = update_type in {"detail", "all"}
|
|
146
|
+
include_orders = update_type in {"orders", "all"}
|
|
147
|
+
include_history = update_type in {"history_order", "history_orders", "all"}
|
|
148
|
+
include_account = update_type in {"account", "positions", "all"}
|
|
149
|
+
account_index = self.account_index
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if include_detail:
|
|
153
|
+
tasks.append(("detail", self.order_api.order_books()))
|
|
154
|
+
|
|
155
|
+
if include_orders:
|
|
156
|
+
if account_index is None or symbol is None:
|
|
157
|
+
if update_type == "orders":
|
|
158
|
+
raise ValueError("account_index and symbol are required to update orders")
|
|
159
|
+
else:
|
|
160
|
+
cid = self.get_contract_id(symbol)
|
|
161
|
+
tasks.append(
|
|
162
|
+
(
|
|
163
|
+
"orders",
|
|
164
|
+
self.order_api.account_active_orders(
|
|
165
|
+
account_index=account_index,
|
|
166
|
+
market_id=int(cid),
|
|
167
|
+
auth=self.auth
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if include_history:
|
|
173
|
+
if account_index is None:
|
|
174
|
+
raise ValueError("account_index is required to update history orders")
|
|
175
|
+
else:
|
|
176
|
+
tasks.append(
|
|
177
|
+
(
|
|
178
|
+
"history_orders",
|
|
179
|
+
self.order_api.account_inactive_orders(
|
|
180
|
+
account_index=account_index,
|
|
181
|
+
limit=limit,
|
|
182
|
+
auth=self.auth
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if include_account:
|
|
188
|
+
if account_index is None:
|
|
189
|
+
if update_type in {"account", "positions"}:
|
|
190
|
+
raise ValueError("account_index is required to update account data")
|
|
191
|
+
else:
|
|
192
|
+
tasks.append(
|
|
193
|
+
(
|
|
194
|
+
"account",
|
|
195
|
+
self.account_api.account(
|
|
196
|
+
by="index",
|
|
197
|
+
value=str(account_index),
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if not tasks:
|
|
203
|
+
logger.debug("No REST requests enqueued for Lighter update_type=%s", update_type)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
results: dict[str, Any] = {}
|
|
207
|
+
for key, coroutine in tasks:
|
|
208
|
+
try:
|
|
209
|
+
results[key] = await coroutine
|
|
210
|
+
except Exception:
|
|
211
|
+
logger.exception("Lighter REST request %s failed", key)
|
|
212
|
+
raise
|
|
213
|
+
|
|
214
|
+
if "detail" in results:
|
|
215
|
+
self.store.detail._onresponse(results["detail"])
|
|
216
|
+
|
|
217
|
+
if "orders" in results:
|
|
218
|
+
self.store.orders._onresponse(results["orders"])
|
|
219
|
+
|
|
220
|
+
if "history_orders" in results:
|
|
221
|
+
self.store.orders._onresponse(results["history_orders"])
|
|
222
|
+
|
|
223
|
+
if "account" in results:
|
|
224
|
+
account_payload = results["account"]
|
|
225
|
+
self.store.accounts._onresponse(account_payload)
|
|
226
|
+
self.store.positions._onresponse(account_payload)
|
|
227
|
+
|
|
228
|
+
async def sub_orderbook(
|
|
229
|
+
self,
|
|
230
|
+
symbols: Sequence[str] | str,
|
|
231
|
+
*,
|
|
232
|
+
account_ids: Sequence[int] | int | None = None,
|
|
233
|
+
depth_limit: int | None = None,
|
|
234
|
+
) -> pybotters.ws.WebSocketApp:
|
|
235
|
+
"""Subscribe to order book (and optional account) websocket streams by symbol."""
|
|
236
|
+
|
|
237
|
+
if isinstance(symbols, str):
|
|
238
|
+
symbol_list = [symbols]
|
|
239
|
+
else:
|
|
240
|
+
symbol_list = list(symbols)
|
|
241
|
+
|
|
242
|
+
if not symbol_list and not account_ids:
|
|
243
|
+
raise ValueError("At least one symbol or account_id must be provided")
|
|
244
|
+
|
|
245
|
+
needs_detail = any(self.get_contract_id(sym) is None for sym in symbol_list)
|
|
246
|
+
if needs_detail and symbol_list:
|
|
247
|
+
try:
|
|
248
|
+
await self.update("detail")
|
|
249
|
+
except Exception:
|
|
250
|
+
logger.exception("Failed to refresh Lighter market metadata for symbol resolution")
|
|
251
|
+
raise
|
|
252
|
+
|
|
253
|
+
order_book_ids: list[str] = []
|
|
254
|
+
for sym in symbol_list:
|
|
255
|
+
market_id = self.get_contract_id(sym)
|
|
256
|
+
if market_id is None:
|
|
257
|
+
if sym.isdigit():
|
|
258
|
+
market_id = sym
|
|
259
|
+
else:
|
|
260
|
+
raise ValueError(f"Unknown symbol: {sym}")
|
|
261
|
+
market_id_str = str(market_id)
|
|
262
|
+
order_book_ids.append(market_id_str)
|
|
263
|
+
self.store.book.id_to_symbol[market_id_str] = sym
|
|
264
|
+
|
|
265
|
+
account_id_list: list[str] = []
|
|
266
|
+
if account_ids is not None:
|
|
267
|
+
if isinstance(account_ids, int):
|
|
268
|
+
account_id_list = [str(account_ids)]
|
|
269
|
+
else:
|
|
270
|
+
account_id_list = [str(aid) for aid in account_ids]
|
|
271
|
+
|
|
272
|
+
if not order_book_ids and not account_id_list:
|
|
273
|
+
raise ValueError("No valid symbols or account_ids resolved for subscription")
|
|
274
|
+
|
|
275
|
+
if depth_limit is not None:
|
|
276
|
+
self.store.book.limit = depth_limit
|
|
277
|
+
|
|
278
|
+
order_book_channels = [f"order_book/{mid}" for mid in order_book_ids]
|
|
279
|
+
account_channels = [f"account_all/{aid}" for aid in account_id_list]
|
|
280
|
+
|
|
281
|
+
send_payload = [
|
|
282
|
+
{"type": "subscribe", "channel": channel} for channel in order_book_channels + account_channels
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
ws_app = self.client.ws_connect(
|
|
286
|
+
self.ws_url,
|
|
287
|
+
send_json=send_payload,
|
|
288
|
+
hdlr_json=self.store.onmessage,
|
|
289
|
+
autoping=False,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
await ws_app._event.wait()
|
|
293
|
+
return ws_app
|
|
294
|
+
|
|
295
|
+
async def sub_accounts(
|
|
296
|
+
self,
|
|
297
|
+
account_ids: Sequence[int] | int,
|
|
298
|
+
) -> pybotters.ws.WebSocketApp:
|
|
299
|
+
"""Subscribe to account-only websocket updates."""
|
|
300
|
+
|
|
301
|
+
return await self.sub_orderbook(market_ids=[], account_ids=account_ids)
|
|
302
|
+
|
|
303
|
+
async def place_order(
|
|
304
|
+
self,
|
|
305
|
+
symbol: str,
|
|
306
|
+
*,
|
|
307
|
+
base_amount: float,
|
|
308
|
+
price: float,
|
|
309
|
+
is_ask: bool,
|
|
310
|
+
order_type: Literal[
|
|
311
|
+
"limit",
|
|
312
|
+
"market",
|
|
313
|
+
"stop-loss",
|
|
314
|
+
"stop-loss-limit",
|
|
315
|
+
"take-profit",
|
|
316
|
+
"take-profit-limit",
|
|
317
|
+
"twap",
|
|
318
|
+
] = "limit",
|
|
319
|
+
time_in_force: Literal["ioc", "gtc", "post_only"] = "gtc",
|
|
320
|
+
reduce_only: bool = False,
|
|
321
|
+
trigger_price: float | None = None,
|
|
322
|
+
order_expiry: int | None = None,
|
|
323
|
+
nonce: int | None = None,
|
|
324
|
+
api_key_index: int | None = None,
|
|
325
|
+
client_order_index: int = 0,
|
|
326
|
+
) -> dict[str, Any]:
|
|
327
|
+
"""Submit an order through the signer client using human-readable inputs."""
|
|
328
|
+
|
|
329
|
+
if self.signer is None:
|
|
330
|
+
raise RuntimeError("SignerClient is required for placing orders")
|
|
331
|
+
|
|
332
|
+
market_index = self.get_contract_id(symbol)
|
|
333
|
+
if market_index is None:
|
|
334
|
+
raise ValueError(f"Unknown symbol: {symbol}")
|
|
335
|
+
market_index = int(market_index)
|
|
336
|
+
|
|
337
|
+
detail = self._get_detail_entry(symbol=symbol, market_index=market_index)
|
|
338
|
+
if detail is None:
|
|
339
|
+
await self.update("detail")
|
|
340
|
+
detail = self._get_detail_entry(symbol=symbol, market_index=market_index)
|
|
341
|
+
if detail is None:
|
|
342
|
+
raise ValueError(f"Market metadata unavailable for symbol: {symbol}")
|
|
343
|
+
|
|
344
|
+
order_type_map = {
|
|
345
|
+
"limit": self.signer.ORDER_TYPE_LIMIT,
|
|
346
|
+
"market": self.signer.ORDER_TYPE_MARKET,
|
|
347
|
+
"stop-loss": self.signer.ORDER_TYPE_STOP_LOSS,
|
|
348
|
+
"stop-loss-limit": self.signer.ORDER_TYPE_STOP_LOSS_LIMIT,
|
|
349
|
+
"take-profit": self.signer.ORDER_TYPE_TAKE_PROFIT,
|
|
350
|
+
"take-profit-limit": self.signer.ORDER_TYPE_TAKE_PROFIT_LIMIT,
|
|
351
|
+
"twap": self.signer.ORDER_TYPE_TWAP,
|
|
352
|
+
}
|
|
353
|
+
tif_map = {
|
|
354
|
+
"ioc": self.signer.ORDER_TIME_IN_FORCE_IMMEDIATE_OR_CANCEL,
|
|
355
|
+
"gtc": self.signer.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME,
|
|
356
|
+
"post_only": self.signer.ORDER_TIME_IN_FORCE_POST_ONLY,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
order_type_code = order_type_map[order_type]
|
|
361
|
+
except KeyError as exc:
|
|
362
|
+
raise ValueError(f"Unsupported order_type: {order_type}") from exc
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
tif_code = tif_map[time_in_force]
|
|
366
|
+
except KeyError as exc:
|
|
367
|
+
raise ValueError(f"Unsupported time_in_force: {time_in_force}") from exc
|
|
368
|
+
|
|
369
|
+
expiry = order_expiry if order_expiry is not None else self.signer.DEFAULT_28_DAY_ORDER_EXPIRY
|
|
370
|
+
nonce_value = nonce if nonce is not None else -1
|
|
371
|
+
api_key_idx = api_key_index if api_key_index is not None else self.api_key_index
|
|
372
|
+
|
|
373
|
+
price_decimals = (
|
|
374
|
+
detail.get("supported_price_decimals")
|
|
375
|
+
or detail.get("price_decimals")
|
|
376
|
+
or detail.get("quote_decimals")
|
|
377
|
+
or 0
|
|
378
|
+
)
|
|
379
|
+
size_decimals = (
|
|
380
|
+
detail.get("supported_size_decimals")
|
|
381
|
+
or detail.get("size_decimals")
|
|
382
|
+
or detail.get("supported_quote_decimals")
|
|
383
|
+
or 0
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
price_scale = 10 ** int(price_decimals)
|
|
387
|
+
size_scale = 10 ** int(size_decimals)
|
|
388
|
+
|
|
389
|
+
price_int = int(round(float(price) * price_scale))
|
|
390
|
+
base_amount_int = int(round(float(base_amount) * size_scale))
|
|
391
|
+
trigger_price_int = (
|
|
392
|
+
int(round(float(trigger_price) * price_scale))
|
|
393
|
+
if trigger_price is not None
|
|
394
|
+
else self.signer.NIL_TRIGGER_PRICE
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
created_tx, response, error = await self.signer.create_order(
|
|
398
|
+
market_index=market_index,
|
|
399
|
+
client_order_index=client_order_index,
|
|
400
|
+
base_amount=base_amount_int,
|
|
401
|
+
price=price_int,
|
|
402
|
+
is_ask=is_ask,
|
|
403
|
+
order_type=order_type_code,
|
|
404
|
+
time_in_force=tif_code,
|
|
405
|
+
reduce_only=reduce_only,
|
|
406
|
+
trigger_price=trigger_price_int,
|
|
407
|
+
order_expiry=expiry,
|
|
408
|
+
nonce=nonce_value,
|
|
409
|
+
api_key_index=api_key_idx,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if error:
|
|
413
|
+
raise RuntimeError(f"Lighter create_order failed: {error}")
|
|
414
|
+
if response is None:
|
|
415
|
+
raise RuntimeError("Lighter create_order returned no response")
|
|
416
|
+
|
|
417
|
+
if hasattr(created_tx, "to_json"):
|
|
418
|
+
request_payload = json.loads(created_tx.to_json())
|
|
419
|
+
else:
|
|
420
|
+
request_payload = str(created_tx)
|
|
421
|
+
response_payload = response.to_dict() if hasattr(response, "to_dict") else _maybe_to_dict(response)
|
|
422
|
+
|
|
423
|
+
# return {
|
|
424
|
+
# "request": request_payload,
|
|
425
|
+
# "response": response_payload,
|
|
426
|
+
# }
|
|
427
|
+
return response_payload
|
|
428
|
+
|
|
429
|
+
async def cancel_order(
|
|
430
|
+
self,
|
|
431
|
+
symbol: str,
|
|
432
|
+
order_index: int,
|
|
433
|
+
*,
|
|
434
|
+
nonce: int | None = None,
|
|
435
|
+
api_key_index: int | None = None,
|
|
436
|
+
) -> dict[str, Any]:
|
|
437
|
+
"""Cancel a single order using the signer client."""
|
|
438
|
+
|
|
439
|
+
market_index = self.get_contract_id(symbol)
|
|
440
|
+
if market_index is None:
|
|
441
|
+
raise ValueError(f"Unknown symbol: {symbol}")
|
|
442
|
+
market_index = int(market_index)
|
|
443
|
+
|
|
444
|
+
if self.signer is None:
|
|
445
|
+
raise RuntimeError("SignerClient is required for cancelling orders")
|
|
446
|
+
|
|
447
|
+
nonce_value = nonce if nonce is not None else -1
|
|
448
|
+
api_key_idx = api_key_index or self.api_key_index
|
|
449
|
+
|
|
450
|
+
cancel_tx, response, error = await self.signer.cancel_order(
|
|
451
|
+
market_index=market_index,
|
|
452
|
+
order_index=order_index,
|
|
453
|
+
nonce=nonce_value,
|
|
454
|
+
api_key_index=api_key_idx,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if error:
|
|
458
|
+
raise RuntimeError(f"Lighter cancel_order failed: {error}")
|
|
459
|
+
if response is None:
|
|
460
|
+
raise RuntimeError("Lighter cancel_order returned no response")
|
|
461
|
+
|
|
462
|
+
if hasattr(cancel_tx, "to_json"):
|
|
463
|
+
request_payload = json.loads(cancel_tx.to_json())
|
|
464
|
+
else:
|
|
465
|
+
request_payload = str(cancel_tx)
|
|
466
|
+
response_payload = response.to_dict() if hasattr(response, "to_dict") else _maybe_to_dict(response)
|
|
467
|
+
return {
|
|
468
|
+
"request": request_payload,
|
|
469
|
+
"response": response_payload,
|
|
470
|
+
}
|