zklighter-perps 1.0.261 → 1.0.262
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.
- package/.circleci/config.yml +26 -11
- package/.circleci/openapi_postprocess.py +273 -129
- package/README.md +80 -0
- package/package.json +7 -3
- package/tsconfig.json +35 -0
package/.circleci/config.yml
CHANGED
|
@@ -30,17 +30,9 @@ jobs:
|
|
|
30
30
|
sed 's/struct{}/{}/g' server.api > temp.txt && cp temp.txt server.api && rm temp.txt
|
|
31
31
|
goctl api plugin --plugin ./goctl-swagger-amd="swagger -filename openapi.json -host mainnet.zklighter.elliot.ai -schemes https" --api server.api --dir .
|
|
32
32
|
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cd zklighter-perps/service/apiserver
|
|
37
|
-
jq 'del(.paths["/api/v1/feedback"], .definitions["ReqSendFeedback"], .paths["/api/v1/ws_status"], .paths["/stream"], .paths["/api/v1/permission"])' openapi.json > temp.json && mv temp.json openapi.json
|
|
38
|
-
jq 'walk(if type == "object" then with_entries(select(.key != "")) else . end)' openapi.json > temp.json && mv temp.json openapi.json
|
|
39
|
-
jq 'walk(if type == "object" and has("summary") then .description = .summary else . end)' openapi.json > temp.json && mv temp.json openapi.json
|
|
40
|
-
jq 'walk(if type == "object" and has("operationId") then .summary = .operationId else . end)' openapi.json > temp.json && mv temp.json openapi.json
|
|
41
|
-
jq 'walk(if type == "object" and has("responses") then .responses["400"] = { "description": "Bad request", "schema": { "$ref": "#/definitions/ResultCode" } } else . end)' openapi.json > tmp.json && mv tmp.json openapi.json
|
|
42
|
-
jq 'walk(if type == "object" and .name == "types" then .type="array" | .items={"type":"integer", "format":"uint8"} | del(.format) else . end)' openapi.json > tmp.json && mv tmp.json openapi.json
|
|
43
|
-
sed 's/"-",//g' openapi.json > temp.txt && mv temp.txt openapi.json
|
|
33
|
+
# NOTE: all openapi.json post-processing now lives in the dedicated
|
|
34
|
+
# `openapi_postprocess` job (.circleci/openapi_postprocess.py). This job
|
|
35
|
+
# only generates and persists the raw spec. See README.md.
|
|
44
36
|
|
|
45
37
|
- store_artifacts:
|
|
46
38
|
path: ~/project/zklighter-perps/service/apiserver/openapi.json
|
|
@@ -133,6 +125,23 @@ jobs:
|
|
|
133
125
|
PR_URL=$(curl -X POST -H "Authorization: Bearer ${GITHUB_TOKEN}" -d '{"title": "'$BE_BRANCH'", "head": "'$BRANCH_NAME'", "base": "main"}' https://api.github.com/repos/elliottech/zklighter-perps-ts/pulls | jq -r '.html_url')
|
|
134
126
|
curl -X POST -H 'Content-type: application/json' --data '{"text":"TypeScript SDK has been updated. Check the PR here: '$PR_URL'", "type": "mrkdwn"}' ${SLACK_URL}
|
|
135
127
|
|
|
128
|
+
typecheck:
|
|
129
|
+
docker:
|
|
130
|
+
- image: cimg/node:22.4.1
|
|
131
|
+
|
|
132
|
+
working_directory: ~/project
|
|
133
|
+
|
|
134
|
+
steps:
|
|
135
|
+
- checkout
|
|
136
|
+
- run:
|
|
137
|
+
name: Install dependencies
|
|
138
|
+
command: |
|
|
139
|
+
npm install
|
|
140
|
+
- run:
|
|
141
|
+
name: Type-check generated SDK
|
|
142
|
+
command: |
|
|
143
|
+
npm run typecheck
|
|
144
|
+
|
|
136
145
|
update_npm_package:
|
|
137
146
|
docker:
|
|
138
147
|
- image: cimg/node:22.4.1
|
|
@@ -186,8 +195,14 @@ workflows:
|
|
|
186
195
|
when:
|
|
187
196
|
not: << pipeline.parameters.update_ts_sdk >>
|
|
188
197
|
jobs:
|
|
198
|
+
# Runs on every branch (including the timestamped branches opened by the
|
|
199
|
+
# update_ts_sdk workflow) so a regenerated SDK that would break consumers
|
|
200
|
+
# fails its PR check before merge.
|
|
201
|
+
- typecheck
|
|
189
202
|
- update_npm_package:
|
|
190
203
|
filters:
|
|
191
204
|
branches:
|
|
192
205
|
only: main
|
|
206
|
+
requires:
|
|
207
|
+
- typecheck
|
|
193
208
|
|
|
@@ -1,139 +1,283 @@
|
|
|
1
|
-
|
|
1
|
+
"""Post-process the swagger/openapi.json before the TypeScript SDK is generated.
|
|
2
|
+
|
|
3
|
+
This is the single source of truth for every transformation applied to the
|
|
4
|
+
spec emitted by goctl-swagger. The raw spec has a number of quirks (empty keys,
|
|
5
|
+
non-standard int16 types, missing error responses, placeholder enum values,
|
|
6
|
+
auto-generated operation ids, etc.) that either break the OpenAPI Generator or
|
|
7
|
+
produce an awkward SDK. See README.md ("OpenAPI post-processing") for the
|
|
8
|
+
rationale behind each step.
|
|
9
|
+
|
|
10
|
+
Previously some of these fixes lived as inline `jq`/`sed` commands in the
|
|
11
|
+
`update_openapi` CI job. They have been consolidated here so the logic is in one
|
|
12
|
+
place, testable, and easy to reason about.
|
|
13
|
+
|
|
14
|
+
The steps below run in the same order they used to run in the pipeline:
|
|
15
|
+
the former `jq`/`sed` transforms first, then the original Python transforms.
|
|
16
|
+
"""
|
|
2
17
|
|
|
18
|
+
import json
|
|
3
19
|
|
|
4
20
|
FILE = "./openapi.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def walk(node, fn):
|
|
24
|
+
"""Apply ``fn`` to every node bottom-up, mirroring jq's ``walk``.
|
|
25
|
+
|
|
26
|
+
Children are transformed before their parent so ``fn`` always sees
|
|
27
|
+
already-processed sub-trees, matching the semantics of the jq commands
|
|
28
|
+
these helpers replaced.
|
|
29
|
+
"""
|
|
30
|
+
if isinstance(node, dict):
|
|
31
|
+
node = {key: walk(value, fn) for key, value in node.items()}
|
|
32
|
+
elif isinstance(node, list):
|
|
33
|
+
node = [walk(item, fn) for item in node]
|
|
34
|
+
return fn(node)
|
|
35
|
+
|
|
36
|
+
|
|
5
37
|
with open(FILE, "r") as f:
|
|
6
38
|
data = json.load(f)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Step 1. Drop endpoints/definitions that must not be part of the public SDK.
|
|
42
|
+
# (was: jq 'del(.paths[...], .definitions["ReqSendFeedback"])')
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
for dropped_path in [
|
|
45
|
+
"/api/v1/feedback",
|
|
46
|
+
"/api/v1/ws_status",
|
|
47
|
+
"/stream",
|
|
48
|
+
"/api/v1/permission",
|
|
49
|
+
]:
|
|
50
|
+
data.get("paths", {}).pop(dropped_path, None)
|
|
51
|
+
data.get("definitions", {}).pop("ReqSendFeedback", None)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Step 2. Remove empty-string keys emitted by the swagger generator, which the
|
|
56
|
+
# OpenAPI Generator cannot handle.
|
|
57
|
+
# (was: jq 'walk(... with_entries(select(.key != "")) ...)')
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
def _strip_empty_keys(node):
|
|
60
|
+
if isinstance(node, dict):
|
|
61
|
+
return {key: value for key, value in node.items() if key != ""}
|
|
62
|
+
return node
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
data = walk(data, _strip_empty_keys)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Step 3. Mirror `summary` into `description` so generated SDK doc comments keep
|
|
70
|
+
# the original human-readable text before `summary` is overwritten below.
|
|
71
|
+
# (was: jq 'walk(if has("summary") then .description = .summary end)')
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
def _summary_to_description(node):
|
|
74
|
+
if isinstance(node, dict) and "summary" in node:
|
|
75
|
+
node["description"] = node["summary"]
|
|
76
|
+
return node
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
data = walk(data, _summary_to_description)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Step 4. Seed `summary` from `operationId` (Step 8 may refine it per-path).
|
|
84
|
+
# (was: jq 'walk(if has("operationId") then .summary = .operationId end)')
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
def _operation_id_to_summary(node):
|
|
87
|
+
if isinstance(node, dict) and "operationId" in node:
|
|
88
|
+
node["summary"] = node["operationId"]
|
|
89
|
+
return node
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
data = walk(data, _operation_id_to_summary)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Step 5. Give every operation a documented 400 response referencing ResultCode.
|
|
97
|
+
# (was: jq 'walk(if has("responses") then .responses["400"] = {...} end)')
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
def _add_bad_request_response(node):
|
|
100
|
+
if isinstance(node, dict) and isinstance(node.get("responses"), dict):
|
|
101
|
+
node["responses"]["400"] = {
|
|
102
|
+
"description": "Bad request",
|
|
103
|
+
"schema": {"$ref": "#/definitions/ResultCode"},
|
|
104
|
+
}
|
|
105
|
+
return node
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
data = walk(data, _add_bad_request_response)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# Step 6. Model the `types` parameter as a byte array (array of uint8) instead of
|
|
113
|
+
# the non-standard scalar the spec declares.
|
|
114
|
+
# (was: jq 'walk(if .name == "types" then .type="array" | .items=... | del(.format) end)')
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
def _types_param_to_byte_array(node):
|
|
117
|
+
if isinstance(node, dict) and node.get("name") == "types":
|
|
118
|
+
node["type"] = "array"
|
|
119
|
+
node["items"] = {"type": "integer", "format": "uint8"}
|
|
120
|
+
node.pop("format", None)
|
|
121
|
+
return node
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
data = walk(data, _types_param_to_byte_array)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Step 7. Remove placeholder "-" values from enum/array lists.
|
|
129
|
+
# The former `sed 's/"-",//g'` only stripped a "-" element when it was
|
|
130
|
+
# followed by another element (i.e. not the last item); we preserve that
|
|
131
|
+
# behaviour here.
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
def _drop_dash_values(node):
|
|
134
|
+
if isinstance(node, list):
|
|
135
|
+
last_index = len(node) - 1
|
|
136
|
+
return [
|
|
137
|
+
value
|
|
138
|
+
for index, value in enumerate(node)
|
|
139
|
+
if not (value == "-" and index != last_index)
|
|
140
|
+
]
|
|
141
|
+
return node
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
data = walk(data, _drop_dash_values)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# Step 8. Derive stable, path-based operationId/summary so generated method
|
|
149
|
+
# names are predictable (e.g. /api/v1/account -> "account").
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
for path in data["paths"]:
|
|
152
|
+
methods = list(data["paths"][path].keys())
|
|
153
|
+
has_multiple_methods = len(methods) > 1
|
|
154
|
+
|
|
155
|
+
for method in methods:
|
|
156
|
+
if "api/v1/" in path:
|
|
157
|
+
base_name = path.split("api/v1/")[1].replace("/", "_")
|
|
158
|
+
data["paths"][path][method]["summary"] = base_name
|
|
159
|
+
if has_multiple_methods:
|
|
160
|
+
data["paths"][path][method]["operationId"] = f"{base_name}_{method}"
|
|
161
|
+
else:
|
|
162
|
+
data["paths"][path][method]["operationId"] = base_name
|
|
163
|
+
elif "api/v2/" in path:
|
|
164
|
+
base_name = path.split("api/v2/")[1].replace("/", "_") + "_v2"
|
|
165
|
+
data["paths"][path][method]["summary"] = base_name
|
|
166
|
+
if has_multiple_methods:
|
|
167
|
+
data["paths"][path][method]["operationId"] = f"{base_name}_{method}"
|
|
26
168
|
else:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if data["paths"][path][method]["summary"] == "":
|
|
35
|
-
data["paths"][path][method]["summary"] = "status"
|
|
36
|
-
if has_multiple_methods:
|
|
37
|
-
data["paths"][path][method]["operationId"] = f"status_{method}"
|
|
38
|
-
else:
|
|
39
|
-
data["paths"][path][method]["operationId"] = "status"
|
|
40
|
-
|
|
41
|
-
# Replace $ref to int16 types with inline equivalents across all definitions.
|
|
42
|
-
# int16 and derived map types are not standard OpenAPI and cause the generator
|
|
43
|
-
# to emit broken model references.
|
|
44
|
-
def replace_int16_refs(obj):
|
|
45
|
-
if isinstance(obj, dict):
|
|
46
|
-
if obj.get("$ref") == "#/definitions/int16":
|
|
47
|
-
return {"type": "integer", "format": "int64"}
|
|
48
|
-
if obj.get("$ref") == "#/definitions/mapint16string":
|
|
49
|
-
return {"type": "object", "additionalProperties": {"type": "string"}}
|
|
50
|
-
if obj.get("$ref") == "#/definitions/mapstringfloat64":
|
|
51
|
-
return {"type": "object", "additionalProperties": {"type": "number", "format": "double"}}
|
|
52
|
-
return {k: replace_int16_refs(v) for k, v in obj.items()}
|
|
53
|
-
if isinstance(obj, list):
|
|
54
|
-
return [replace_int16_refs(i) for i in obj]
|
|
55
|
-
return obj
|
|
56
|
-
|
|
57
|
-
for defn_name in list(data["definitions"]):
|
|
58
|
-
data["definitions"][defn_name] = replace_int16_refs(
|
|
59
|
-
data["definitions"][defn_name]
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
# Fix enum placement for array types: move enum from array level into items
|
|
63
|
-
for defn in data["definitions"].values():
|
|
64
|
-
for prop in defn.get("properties", {}).values():
|
|
65
|
-
if prop.get("type") == "array" and "enum" in prop:
|
|
66
|
-
prop["items"]["enum"] = prop.pop("enum")
|
|
67
|
-
|
|
68
|
-
# transfer_history type parameter to be array
|
|
69
|
-
if "/api/v1/transfer/history" in data["paths"]:
|
|
70
|
-
for method in data["paths"]["/api/v1/transfer/history"].values():
|
|
71
|
-
for param in method.get("parameters", []):
|
|
72
|
-
if (
|
|
73
|
-
param.get("name") == "type"
|
|
74
|
-
and param.get("type") == "string"
|
|
75
|
-
and "enum" in param
|
|
76
|
-
):
|
|
77
|
-
param["type"] = "array"
|
|
78
|
-
param["items"] = {"type": "string", "enum": param.pop("enum")}
|
|
79
|
-
break
|
|
80
|
-
|
|
81
|
-
for path in data["definitions"]:
|
|
82
|
-
if not path.startswith("Req"):
|
|
83
|
-
required_fields = list(data["definitions"][path]["properties"].keys())
|
|
84
|
-
if "message" in required_fields:
|
|
85
|
-
required_fields.remove("message")
|
|
86
|
-
|
|
87
|
-
if "next" in required_fields:
|
|
88
|
-
required_fields.remove("next")
|
|
89
|
-
|
|
90
|
-
if "next_cursor" in required_fields:
|
|
91
|
-
required_fields.remove("next_cursor")
|
|
92
|
-
|
|
93
|
-
if "account_share" in required_fields:
|
|
94
|
-
required_fields.remove("account_share")
|
|
95
|
-
|
|
96
|
-
if "total_funding_paid_out" in required_fields:
|
|
97
|
-
required_fields.remove("total_funding_paid_out")
|
|
98
|
-
|
|
99
|
-
if "pending_unlocks" in required_fields:
|
|
100
|
-
required_fields.remove("pending_unlocks")
|
|
101
|
-
|
|
102
|
-
if "account_trading_mode" in required_fields:
|
|
103
|
-
required_fields.remove("account_trading_mode")
|
|
104
|
-
|
|
105
|
-
if "funding_fee_discounts_enabled" in required_fields:
|
|
106
|
-
required_fields.remove("funding_fee_discounts_enabled")
|
|
107
|
-
|
|
108
|
-
if "hidden" in required_fields:
|
|
109
|
-
required_fields.remove("hidden")
|
|
110
|
-
|
|
111
|
-
if "approved_integrators" in required_fields:
|
|
112
|
-
required_fields.remove("approved_integrators")
|
|
113
|
-
|
|
114
|
-
if "ask_id_str" in required_fields:
|
|
115
|
-
required_fields.remove("ask_id_str")
|
|
116
|
-
|
|
117
|
-
if "bid_id_str" in required_fields:
|
|
118
|
-
required_fields.remove("bid_id_str")
|
|
119
|
-
|
|
120
|
-
if "market_maker_incentive_account_index" in required_fields:
|
|
121
|
-
required_fields.remove("market_maker_incentive_account_index")
|
|
122
|
-
|
|
123
|
-
if "user_tier_last_update" in required_fields:
|
|
124
|
-
required_fields.remove("user_tier_last_update")
|
|
125
|
-
|
|
126
|
-
if path == "Trade":
|
|
127
|
-
if "taker_fee" in required_fields:
|
|
128
|
-
required_fields.remove("taker_fee")
|
|
129
|
-
|
|
130
|
-
if "maker_fee" in required_fields:
|
|
131
|
-
required_fields.remove("maker_fee")
|
|
132
|
-
|
|
133
|
-
if len(required_fields) > 0:
|
|
134
|
-
data["definitions"][path]["required"] = required_fields
|
|
169
|
+
data["paths"][path][method]["operationId"] = base_name
|
|
170
|
+
else:
|
|
171
|
+
base_name = path.split("/")[-1]
|
|
172
|
+
data["paths"][path][method]["summary"] = base_name
|
|
173
|
+
if has_multiple_methods:
|
|
174
|
+
data["paths"][path][method]["operationId"] = f"{base_name}_{method}"
|
|
135
175
|
else:
|
|
136
|
-
data["
|
|
176
|
+
data["paths"][path][method]["operationId"] = base_name
|
|
177
|
+
|
|
178
|
+
if data["paths"][path][method]["summary"] == "":
|
|
179
|
+
data["paths"][path][method]["summary"] = "status"
|
|
180
|
+
if has_multiple_methods:
|
|
181
|
+
data["paths"][path][method]["operationId"] = f"status_{method}"
|
|
182
|
+
else:
|
|
183
|
+
data["paths"][path][method]["operationId"] = "status"
|
|
184
|
+
|
|
185
|
+
# Replace $ref to int16 types with inline equivalents across all definitions.
|
|
186
|
+
# int16 and derived map types are not standard OpenAPI and cause the generator
|
|
187
|
+
# to emit broken model references.
|
|
188
|
+
def replace_int16_refs(obj):
|
|
189
|
+
if isinstance(obj, dict):
|
|
190
|
+
if obj.get("$ref") == "#/definitions/int16":
|
|
191
|
+
return {"type": "integer", "format": "int64"}
|
|
192
|
+
if obj.get("$ref") == "#/definitions/mapint16string":
|
|
193
|
+
return {"type": "object", "additionalProperties": {"type": "string"}}
|
|
194
|
+
if obj.get("$ref") == "#/definitions/mapstringfloat64":
|
|
195
|
+
return {"type": "object", "additionalProperties": {"type": "number", "format": "double"}}
|
|
196
|
+
return {k: replace_int16_refs(v) for k, v in obj.items()}
|
|
197
|
+
if isinstance(obj, list):
|
|
198
|
+
return [replace_int16_refs(i) for i in obj]
|
|
199
|
+
return obj
|
|
200
|
+
|
|
201
|
+
for defn_name in list(data["definitions"]):
|
|
202
|
+
data["definitions"][defn_name] = replace_int16_refs(
|
|
203
|
+
data["definitions"][defn_name]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Fix enum placement for array types: move enum from array level into items
|
|
207
|
+
for defn in data["definitions"].values():
|
|
208
|
+
for prop in defn.get("properties", {}).values():
|
|
209
|
+
if prop.get("type") == "array" and "enum" in prop:
|
|
210
|
+
prop["items"]["enum"] = prop.pop("enum")
|
|
211
|
+
|
|
212
|
+
# transfer_history type parameter to be array
|
|
213
|
+
if "/api/v1/transfer/history" in data["paths"]:
|
|
214
|
+
for method in data["paths"]["/api/v1/transfer/history"].values():
|
|
215
|
+
for param in method.get("parameters", []):
|
|
216
|
+
if (
|
|
217
|
+
param.get("name") == "type"
|
|
218
|
+
and param.get("type") == "string"
|
|
219
|
+
and "enum" in param
|
|
220
|
+
):
|
|
221
|
+
param["type"] = "array"
|
|
222
|
+
param["items"] = {"type": "string", "enum": param.pop("enum")}
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
for path in data["definitions"]:
|
|
226
|
+
if not path.startswith("Req"):
|
|
227
|
+
required_fields = list(data["definitions"][path]["properties"].keys())
|
|
228
|
+
if "message" in required_fields:
|
|
229
|
+
required_fields.remove("message")
|
|
230
|
+
|
|
231
|
+
if "next" in required_fields:
|
|
232
|
+
required_fields.remove("next")
|
|
233
|
+
|
|
234
|
+
if "next_cursor" in required_fields:
|
|
235
|
+
required_fields.remove("next_cursor")
|
|
236
|
+
|
|
237
|
+
if "account_share" in required_fields:
|
|
238
|
+
required_fields.remove("account_share")
|
|
239
|
+
|
|
240
|
+
if "total_funding_paid_out" in required_fields:
|
|
241
|
+
required_fields.remove("total_funding_paid_out")
|
|
242
|
+
|
|
243
|
+
if "pending_unlocks" in required_fields:
|
|
244
|
+
required_fields.remove("pending_unlocks")
|
|
245
|
+
|
|
246
|
+
if "account_trading_mode" in required_fields:
|
|
247
|
+
required_fields.remove("account_trading_mode")
|
|
248
|
+
|
|
249
|
+
if "funding_fee_discounts_enabled" in required_fields:
|
|
250
|
+
required_fields.remove("funding_fee_discounts_enabled")
|
|
251
|
+
|
|
252
|
+
if "hidden" in required_fields:
|
|
253
|
+
required_fields.remove("hidden")
|
|
254
|
+
|
|
255
|
+
if "approved_integrators" in required_fields:
|
|
256
|
+
required_fields.remove("approved_integrators")
|
|
257
|
+
|
|
258
|
+
if "ask_id_str" in required_fields:
|
|
259
|
+
required_fields.remove("ask_id_str")
|
|
260
|
+
|
|
261
|
+
if "bid_id_str" in required_fields:
|
|
262
|
+
required_fields.remove("bid_id_str")
|
|
263
|
+
|
|
264
|
+
if "market_maker_incentive_account_index" in required_fields:
|
|
265
|
+
required_fields.remove("market_maker_incentive_account_index")
|
|
266
|
+
|
|
267
|
+
if "user_tier_last_update" in required_fields:
|
|
268
|
+
required_fields.remove("user_tier_last_update")
|
|
269
|
+
|
|
270
|
+
if path == "Trade":
|
|
271
|
+
if "taker_fee" in required_fields:
|
|
272
|
+
required_fields.remove("taker_fee")
|
|
273
|
+
|
|
274
|
+
if "maker_fee" in required_fields:
|
|
275
|
+
required_fields.remove("maker_fee")
|
|
276
|
+
|
|
277
|
+
if len(required_fields) > 0:
|
|
278
|
+
data["definitions"][path]["required"] = required_fields
|
|
279
|
+
else:
|
|
280
|
+
data["definitions"][path].pop("required", None)
|
|
137
281
|
|
|
138
282
|
with open(FILE, "w") as f:
|
|
139
283
|
json.dump(data, f, indent=2)
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# zklighter-perps-ts
|
|
2
|
+
|
|
3
|
+
Auto-generated TypeScript SDK for the Lighter Perps API, published to npm as
|
|
4
|
+
[`zklighter-perps`](https://www.npmjs.com/package/zklighter-perps).
|
|
5
|
+
|
|
6
|
+
The SDK source (`index.ts`, `runtime.ts`, `apis/`, `models/`) is **generated** by
|
|
7
|
+
the [OpenAPI Generator](https://openapi-generator.tech) from the backend's
|
|
8
|
+
OpenAPI spec. Do not edit the generated files by hand — they are overwritten on
|
|
9
|
+
every regeneration. The package ships raw `.ts` (`"main": "index.ts"`), so
|
|
10
|
+
consumers compile it themselves.
|
|
11
|
+
|
|
12
|
+
## Generation pipeline (CircleCI)
|
|
13
|
+
|
|
14
|
+
Defined in [`.circleci/config.yml`](.circleci/config.yml). The `update_ts_sdk`
|
|
15
|
+
workflow (manually triggered) runs three jobs in sequence:
|
|
16
|
+
|
|
17
|
+
1. **`update_openapi`** — clones `zklighter-perps`, runs `goctl-swagger` to emit
|
|
18
|
+
the raw `openapi.json`, and persists it. This job no longer transforms the
|
|
19
|
+
spec; it only generates and persists it.
|
|
20
|
+
2. **`openapi_postprocess`** — runs
|
|
21
|
+
[`.circleci/openapi_postprocess.py`](.circleci/openapi_postprocess.py), the
|
|
22
|
+
**single source of truth** for all spec fixups (see below).
|
|
23
|
+
3. **`update_ts_sdk`** — runs the OpenAPI Generator on the post-processed spec,
|
|
24
|
+
bumps the package version, and opens a PR.
|
|
25
|
+
|
|
26
|
+
The `update_npm_package` workflow runs on `main`:
|
|
27
|
+
|
|
28
|
+
- **`typecheck`** — `npm install && npm run typecheck` (`tsc --noEmit` using
|
|
29
|
+
[`tsconfig.json`](tsconfig.json)). Runs on every branch, including the
|
|
30
|
+
timestamped branches opened by `update_ts_sdk`, so a regenerated SDK that
|
|
31
|
+
would fail to compile in a consumer (e.g. `perps-fe`) is caught on its PR.
|
|
32
|
+
- **`update_npm_package`** — publishes to npm (requires `typecheck` to pass).
|
|
33
|
+
|
|
34
|
+
## OpenAPI post-processing
|
|
35
|
+
|
|
36
|
+
The spec emitted by `goctl-swagger` has quirks that either break the OpenAPI
|
|
37
|
+
Generator or produce an awkward SDK, so we post-process it before generation.
|
|
38
|
+
All of this lives in **one place** — `.circleci/openapi_postprocess.py`.
|
|
39
|
+
(Historically some of these fixes were inline `jq`/`sed` commands in the
|
|
40
|
+
`update_openapi` job; they have been consolidated into the script so the logic
|
|
41
|
+
is in a single, testable file.)
|
|
42
|
+
|
|
43
|
+
Why each transform exists:
|
|
44
|
+
|
|
45
|
+
1. **Drop internal endpoints/definitions** (`/api/v1/feedback`,
|
|
46
|
+
`/api/v1/ws_status`, `/stream`, `/api/v1/permission`, `ReqSendFeedback`) —
|
|
47
|
+
not part of the public SDK surface.
|
|
48
|
+
2. **Strip empty-string keys** — the generator emits `""` keys the OpenAPI
|
|
49
|
+
Generator cannot process.
|
|
50
|
+
3. **Mirror `summary` into `description`** — preserves the original
|
|
51
|
+
human-readable text as the generated doc comment before `summary` is
|
|
52
|
+
overwritten in step 8.
|
|
53
|
+
4. **Seed `summary` from `operationId`** — normalizes naming before step 8.
|
|
54
|
+
5. **Add a documented `400` response** referencing `ResultCode` to every
|
|
55
|
+
operation, so the SDK models the standard error shape.
|
|
56
|
+
6. **Model the `types` parameter as a byte array** (`array` of `uint8`) instead
|
|
57
|
+
of the non-standard scalar the spec declares.
|
|
58
|
+
7. **Remove placeholder `"-"` enum/array values** that aren't real values.
|
|
59
|
+
8. **Derive stable, path-based `operationId`/`summary`** (e.g.
|
|
60
|
+
`/api/v1/account` → `account`) so generated method names are predictable.
|
|
61
|
+
9. **Inline non-standard `int16`/map `$ref`s** (`int16`, `mapint16string`,
|
|
62
|
+
`mapstringfloat64`) — these are not standard OpenAPI and make the generator
|
|
63
|
+
emit broken model references.
|
|
64
|
+
10. **Move array-level `enum` into `items`** — correct placement for array
|
|
65
|
+
schemas.
|
|
66
|
+
11. **Make the `transfer/history` `type` param an array** of enum strings.
|
|
67
|
+
12. **Trim over-eager `required` fields** — the backend marks fields required
|
|
68
|
+
that are actually optional in responses; we relax them so deserialization
|
|
69
|
+
doesn't reject valid payloads.
|
|
70
|
+
|
|
71
|
+
## Local development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install # installs TypeScript (the only dependency)
|
|
75
|
+
npm run typecheck # same check CI runs: tsc --noEmit
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
To test the post-processing script against a spec locally, place an
|
|
79
|
+
`openapi.json` next to the script and run `python3 openapi_postprocess.py`
|
|
80
|
+
(it reads/writes `./openapi.json`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zklighter-perps",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.262",
|
|
4
4
|
"description": "Lighter Perps SDK",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"directories": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"test": "test"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
12
13
|
},
|
|
13
14
|
"repository": {
|
|
14
15
|
"type": "git",
|
|
@@ -19,5 +20,8 @@
|
|
|
19
20
|
"bugs": {
|
|
20
21
|
"url": "https://github.com/elliottech/zklighter-perps-ts/issues"
|
|
21
22
|
},
|
|
22
|
-
"homepage": "https://github.com/elliottech/zklighter-perps-ts#readme"
|
|
23
|
+
"homepage": "https://github.com/elliottech/zklighter-perps-ts#readme",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "5.9.3"
|
|
26
|
+
}
|
|
23
27
|
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Strict type-check config used by CI to guarantee the generated SDK source
|
|
3
|
+
// compiles under the same settings consumers (e.g. perps-fe) use to bundle it.
|
|
4
|
+
// We type-check from the package entry point (index.ts) so the program matches
|
|
5
|
+
// exactly what a consumer pulls in via `import ... from 'zklighter-perps'` and
|
|
6
|
+
// its re-exports. Files not reachable from index.ts (orphaned output left over
|
|
7
|
+
// from previous generations and no longer in .openapi-generator/FILES) are not
|
|
8
|
+
// part of the public API surface and are intentionally not checked.
|
|
9
|
+
"compilerOptions": {
|
|
10
|
+
"esModuleInterop": false,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"target": "ESNext",
|
|
13
|
+
"allowJs": false,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUncheckedIndexedAccess": true,
|
|
19
|
+
"noEmit": true,
|
|
20
|
+
"module": "ESNext",
|
|
21
|
+
"moduleResolution": "bundler",
|
|
22
|
+
"lib": [
|
|
23
|
+
"ESNext",
|
|
24
|
+
"DOM",
|
|
25
|
+
"DOM.Iterable"
|
|
26
|
+
],
|
|
27
|
+
"useDefineForClassFields": true,
|
|
28
|
+
"strictNullChecks": true,
|
|
29
|
+
"allowSyntheticDefaultImports": true,
|
|
30
|
+
"forceConsistentCasingInFileNames": true
|
|
31
|
+
},
|
|
32
|
+
"include": [
|
|
33
|
+
"index.ts"
|
|
34
|
+
]
|
|
35
|
+
}
|