@gera2ld/lib-cex 0.0.1
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/dist/index.d.ts +1 -0
- package/dist/index.js +351 -0
- package/dist/requesters/binance.d.ts +14 -0
- package/dist/requesters/crypto-com.d.ts +13 -0
- package/dist/requesters/gemini.d.ts +11 -0
- package/dist/requesters/index.d.ts +6 -0
- package/dist/requesters/okx.d.ts +10 -0
- package/dist/requesters/types.d.ts +8 -0
- package/dist/requesters/util.d.ts +6 -0
- package/package.json +32 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './requesters';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import _ from "node:process";
|
|
2
|
+
import { delay as A } from "es-toolkit";
|
|
3
|
+
import { createHmac as g } from "node:crypto";
|
|
4
|
+
_.env.CGI_DIR;
|
|
5
|
+
function d(n) {
|
|
6
|
+
const s = _.env[n];
|
|
7
|
+
if (!s)
|
|
8
|
+
throw new Error(`Missing environment variable: ${n}`);
|
|
9
|
+
return s;
|
|
10
|
+
}
|
|
11
|
+
const b = "=", K = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
12
|
+
function q(n, s, t) {
|
|
13
|
+
let e = "", i = 0;
|
|
14
|
+
for (; i < n.length; ) {
|
|
15
|
+
const a = n[i++], o = n[i++], r = n[i++];
|
|
16
|
+
if (e += s[a >> 2], e += s[(a & 3) << 4 | (o || 0) >> 4], o == null ? e += b : e += s[(o & 15) << 2 | (r || 0) >> 6], r == null) {
|
|
17
|
+
e += b;
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
e += s[r & 63];
|
|
21
|
+
}
|
|
22
|
+
return e;
|
|
23
|
+
}
|
|
24
|
+
function R(n) {
|
|
25
|
+
return q(n, K);
|
|
26
|
+
}
|
|
27
|
+
function x(n) {
|
|
28
|
+
return new TextEncoder().encode(n);
|
|
29
|
+
}
|
|
30
|
+
const j = {
|
|
31
|
+
maximumFractionDigits: 4,
|
|
32
|
+
exponentialThresholdLarge: 1e6,
|
|
33
|
+
exponentialThresholdSmall: 1e-6,
|
|
34
|
+
minimumSignificantDigits: 2,
|
|
35
|
+
maximumSignificantDigits: 8,
|
|
36
|
+
quantizationStep: 0,
|
|
37
|
+
keepTrailingZeros: !1
|
|
38
|
+
};
|
|
39
|
+
function N(n, s) {
|
|
40
|
+
if (n == null || n === "")
|
|
41
|
+
return "";
|
|
42
|
+
let t = +n;
|
|
43
|
+
if (Number.isNaN(t))
|
|
44
|
+
return "";
|
|
45
|
+
const e = {
|
|
46
|
+
...j,
|
|
47
|
+
...s
|
|
48
|
+
};
|
|
49
|
+
e.quantizationStep > 0 && (t = Math.round(t / e.quantizationStep) * e.quantizationStep);
|
|
50
|
+
const i = Math.abs(t), a = e.maximumSignificantDigits;
|
|
51
|
+
if (t !== 0 && (i >= e.exponentialThresholdLarge || i < e.exponentialThresholdSmall)) {
|
|
52
|
+
let c;
|
|
53
|
+
return e.keepTrailingZeros ? c = t.toExponential(Math.max(0, a - 1)) : (c = t.toPrecision(a), c = (+c).toExponential()), c;
|
|
54
|
+
}
|
|
55
|
+
const o = {
|
|
56
|
+
useGrouping: !1,
|
|
57
|
+
notation: "standard"
|
|
58
|
+
};
|
|
59
|
+
o.minimumSignificantDigits = e.minimumSignificantDigits, o.maximumSignificantDigits = a;
|
|
60
|
+
let r = new Intl.NumberFormat("en-US", o).format(t);
|
|
61
|
+
return !e.keepTrailingZeros && r.includes(".") && (r = r.replace(/\.?0+$/, "")), r;
|
|
62
|
+
}
|
|
63
|
+
const D = [
|
|
64
|
+
["ms", 1],
|
|
65
|
+
["s", 1e3],
|
|
66
|
+
["m", 60],
|
|
67
|
+
["h", 60],
|
|
68
|
+
["d", 24],
|
|
69
|
+
["mo", 30],
|
|
70
|
+
["y", 365 / 30]
|
|
71
|
+
];
|
|
72
|
+
function Y(n, s) {
|
|
73
|
+
const t = Math.sign(n);
|
|
74
|
+
n = Math.abs(n);
|
|
75
|
+
let e = "ms";
|
|
76
|
+
for (const [i, a] of D) {
|
|
77
|
+
if (n < a) break;
|
|
78
|
+
n /= a, e = i;
|
|
79
|
+
}
|
|
80
|
+
return `${t < 0 ? "-" : ""}${N(n, s) || ""}${e}`;
|
|
81
|
+
}
|
|
82
|
+
const T = {};
|
|
83
|
+
function G(n) {
|
|
84
|
+
return new URLSearchParams(
|
|
85
|
+
Object.entries(n || {}).filter(([, s]) => s != null).map(([s, t]) => [s, `${t}`])
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
function k(n) {
|
|
89
|
+
const s = Date.now();
|
|
90
|
+
return () => {
|
|
91
|
+
const t = Date.now() - s;
|
|
92
|
+
console.log(`${n}: ${Y(t)}`);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
class P {
|
|
96
|
+
constructor(s, t, e = "spot") {
|
|
97
|
+
this.apiKey = s, this.apiSecret = t, this.marketType = e;
|
|
98
|
+
}
|
|
99
|
+
static defaultOpts = {
|
|
100
|
+
auth: !0,
|
|
101
|
+
checkResponse: !0,
|
|
102
|
+
method: "GET"
|
|
103
|
+
};
|
|
104
|
+
static create(s = "spot") {
|
|
105
|
+
const t = ["CRYPTO_BINANCE_API_KEY", "CRYPTO_BINANCE_API_SECRET"], e = d(t[0]), i = d(t[1]);
|
|
106
|
+
return new P(e, i, s);
|
|
107
|
+
}
|
|
108
|
+
requestToken = Promise.resolve();
|
|
109
|
+
async _request(s, t) {
|
|
110
|
+
const e = {
|
|
111
|
+
...P.defaultOpts,
|
|
112
|
+
...t
|
|
113
|
+
}, { auth: i, params: a, body: o } = e;
|
|
114
|
+
let { method: r } = e, c;
|
|
115
|
+
r === "GET" && o && (r = "POST", c = {
|
|
116
|
+
...c,
|
|
117
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
118
|
+
});
|
|
119
|
+
let p = a ? new URLSearchParams(a).toString() : "", u = o ? new URLSearchParams(o).toString() : null;
|
|
120
|
+
if (i) {
|
|
121
|
+
const y = `×tamp=${Date.now()}`;
|
|
122
|
+
u ? u += y : p += y;
|
|
123
|
+
const $ = `&signature=${g("sha256", this.apiSecret).update(p + (u || "")).digest("hex")}`;
|
|
124
|
+
u ? u += $ : p += $, c = {
|
|
125
|
+
...c,
|
|
126
|
+
"X-MBX-APIKEY": this.apiKey
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
p && (p = `?${p}`);
|
|
130
|
+
const l = {
|
|
131
|
+
spot: "api",
|
|
132
|
+
"future-u": "fapi",
|
|
133
|
+
"future-c": "dapi"
|
|
134
|
+
}[this.marketType], m = `https://${l}.binance.com/${l}${s}`, f = k(m), h = await fetch(m + p, {
|
|
135
|
+
...T.options,
|
|
136
|
+
method: r,
|
|
137
|
+
headers: c,
|
|
138
|
+
body: u
|
|
139
|
+
}), S = await h.json();
|
|
140
|
+
if (f(), !h.ok) throw { status: h.status, data: S };
|
|
141
|
+
return S;
|
|
142
|
+
}
|
|
143
|
+
async _requestWithRetry(s, t) {
|
|
144
|
+
let e = new Error("Unknown error");
|
|
145
|
+
for (let i = 0; i < 3; i += 1) {
|
|
146
|
+
i > 0 && console.error(`Invalid timestamp, retry ${i}...`);
|
|
147
|
+
try {
|
|
148
|
+
return await this._request(s, t);
|
|
149
|
+
} catch (a) {
|
|
150
|
+
if (e = a, e?.status === 400 && e.data?.code === -1021)
|
|
151
|
+
continue;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
throw e;
|
|
156
|
+
}
|
|
157
|
+
request(s, t) {
|
|
158
|
+
const e = this.requestToken.then(
|
|
159
|
+
() => this._requestWithRetry(s, t)
|
|
160
|
+
);
|
|
161
|
+
return this.requestToken = e.catch(() => {
|
|
162
|
+
}).then(() => A(50)), e;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function w(n) {
|
|
166
|
+
if (n == null) return "";
|
|
167
|
+
if (Array.isArray(n)) return n.map(w).join("");
|
|
168
|
+
if (typeof n == "object") {
|
|
169
|
+
const s = n;
|
|
170
|
+
return Object.keys(s).sort().map((t) => s[t] == null ? "" : t + w(s[t])).join("");
|
|
171
|
+
}
|
|
172
|
+
return `${n}`;
|
|
173
|
+
}
|
|
174
|
+
function M(n, s, t) {
|
|
175
|
+
const { id: e, method: i, params: a, nonce: o } = t, r = w(a), c = i + e + n + r + o;
|
|
176
|
+
return g("sha256", s).update(c).digest("hex");
|
|
177
|
+
}
|
|
178
|
+
class E {
|
|
179
|
+
constructor(s, t) {
|
|
180
|
+
this.apiKey = s, this.apiSecret = t;
|
|
181
|
+
}
|
|
182
|
+
static defaultOpts = {
|
|
183
|
+
auth: !0,
|
|
184
|
+
checkResponse: !0,
|
|
185
|
+
method: "GET"
|
|
186
|
+
};
|
|
187
|
+
static create(s) {
|
|
188
|
+
let t = ["CRYPTO_CDC_API_KEY", "CRYPTO_CDC_API_SECRET"];
|
|
189
|
+
s && (t = t.map((a) => `${a}_${s}`));
|
|
190
|
+
const e = d(t[0]), i = d(t[1]);
|
|
191
|
+
return new E(e, i);
|
|
192
|
+
}
|
|
193
|
+
requestId = 0;
|
|
194
|
+
async request(s, t) {
|
|
195
|
+
const e = {
|
|
196
|
+
...E.defaultOpts,
|
|
197
|
+
...t
|
|
198
|
+
}, { auth: i, params: a } = e;
|
|
199
|
+
let { method: o } = e;
|
|
200
|
+
const r = {
|
|
201
|
+
method: s,
|
|
202
|
+
params: e.body
|
|
203
|
+
};
|
|
204
|
+
if (i) {
|
|
205
|
+
const { requestId: f } = this;
|
|
206
|
+
this.requestId += 1;
|
|
207
|
+
const h = Date.now();
|
|
208
|
+
Object.assign(r, {
|
|
209
|
+
id: f,
|
|
210
|
+
nonce: h,
|
|
211
|
+
api_key: this.apiKey
|
|
212
|
+
}), r.sig = M(this.apiKey, this.apiSecret, r);
|
|
213
|
+
}
|
|
214
|
+
const c = r.params || r.sig ? JSON.stringify(r) : null;
|
|
215
|
+
o === "GET" && c && (o = "POST");
|
|
216
|
+
const p = a ? "?" + new URLSearchParams(
|
|
217
|
+
Object.entries(a).map(([f, h]) => [f, `${h}`])
|
|
218
|
+
).toString() : "", u = `https://api.crypto.com/v2/${s}${p}`, l = await fetch(u, {
|
|
219
|
+
...T.options,
|
|
220
|
+
method: o,
|
|
221
|
+
headers: {
|
|
222
|
+
"Content-Type": "application/json"
|
|
223
|
+
},
|
|
224
|
+
body: c
|
|
225
|
+
}), m = await l.json();
|
|
226
|
+
if (!e.ignoreError && (!l.ok || m.code))
|
|
227
|
+
throw { status: l.status, data: m };
|
|
228
|
+
return m;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
class O {
|
|
232
|
+
constructor(s, t) {
|
|
233
|
+
this.apiKey = s, this.apiSecret = t;
|
|
234
|
+
}
|
|
235
|
+
static defaultOpts = {
|
|
236
|
+
auth: !0,
|
|
237
|
+
checkResponse: !0
|
|
238
|
+
};
|
|
239
|
+
static create(s) {
|
|
240
|
+
let t = ["CRYPTO_GEMINI_API_KEY", "CRYPTO_GEMINI_API_SECRET"];
|
|
241
|
+
s && (t = t.map((a) => `${a}_${s}`));
|
|
242
|
+
const e = d(t[0]), i = d(t[1]);
|
|
243
|
+
return new O(e, i);
|
|
244
|
+
}
|
|
245
|
+
requestToken = {
|
|
246
|
+
public: Promise.resolve(),
|
|
247
|
+
private: Promise.resolve()
|
|
248
|
+
};
|
|
249
|
+
async doRequest(s, t) {
|
|
250
|
+
const { auth: e, params: i, body: a } = t;
|
|
251
|
+
let o = "";
|
|
252
|
+
const r = {
|
|
253
|
+
...T.options
|
|
254
|
+
};
|
|
255
|
+
if (i && (o = new URLSearchParams(
|
|
256
|
+
Object.entries(i).map(([f, h]) => [f, `${h}`])
|
|
257
|
+
).toString(), o && (o = `?${o}`)), e) {
|
|
258
|
+
const m = {
|
|
259
|
+
request: s,
|
|
260
|
+
nonce: Date.now() / 1e3,
|
|
261
|
+
...a
|
|
262
|
+
}, f = R(x(JSON.stringify(m))), h = g("sha384", this.apiSecret).update(f).digest("hex");
|
|
263
|
+
Object.assign(r, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers: {
|
|
266
|
+
"content-type": "text/plain",
|
|
267
|
+
"X-GEMINI-APIKEY": this.apiKey,
|
|
268
|
+
"X-GEMINI-PAYLOAD": f,
|
|
269
|
+
"X-GEMINI-SIGNATURE": h
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const c = `https://api.gemini.com${s}`, p = k(c), u = await fetch(c + o, r), l = await u.json();
|
|
274
|
+
if (p(), !u.ok) throw { status: u.status, data: l };
|
|
275
|
+
return l;
|
|
276
|
+
}
|
|
277
|
+
request(s, t) {
|
|
278
|
+
const e = {
|
|
279
|
+
...O.defaultOpts,
|
|
280
|
+
...t
|
|
281
|
+
}, i = e.auth ? "private" : "public", a = this.requestToken[i], o = a.then(() => this.doRequest(s, e));
|
|
282
|
+
return this.requestToken[i] = Promise.allSettled([
|
|
283
|
+
o,
|
|
284
|
+
a.then(() => A(e.auth ? 200 : 1e3))
|
|
285
|
+
]).then(() => {
|
|
286
|
+
}), o;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
class I {
|
|
290
|
+
constructor(s, t, e) {
|
|
291
|
+
this.apiKey = s, this.apiSecret = t, this.apiPass = e;
|
|
292
|
+
}
|
|
293
|
+
static defaultOpts = {
|
|
294
|
+
auth: !0,
|
|
295
|
+
method: "GET",
|
|
296
|
+
checkResponse: !0
|
|
297
|
+
};
|
|
298
|
+
static create(s) {
|
|
299
|
+
let t = [
|
|
300
|
+
"CRYPTO_OKX_API_KEY",
|
|
301
|
+
"CRYPTO_OKX_API_SECRET",
|
|
302
|
+
"CRYPTO_OKX_API_PASS"
|
|
303
|
+
];
|
|
304
|
+
s && (t = t.map((o) => `${o}_${s}`));
|
|
305
|
+
const e = d(t[0]), i = d(t[1]), a = d(t[2]);
|
|
306
|
+
return new I(e, i, a);
|
|
307
|
+
}
|
|
308
|
+
async request(s, t) {
|
|
309
|
+
const e = {
|
|
310
|
+
...I.defaultOpts,
|
|
311
|
+
...t
|
|
312
|
+
}, { auth: i, params: a, checkResponse: o } = e;
|
|
313
|
+
let { method: r } = e;
|
|
314
|
+
const c = (/* @__PURE__ */ new Date()).toISOString(), p = e.body == null ? null : JSON.stringify(e.body);
|
|
315
|
+
r === "GET" && p && (r = "POST");
|
|
316
|
+
let u = G(a).toString();
|
|
317
|
+
u && (u = `?${u}`);
|
|
318
|
+
const l = {
|
|
319
|
+
"Content-Type": "application/json"
|
|
320
|
+
};
|
|
321
|
+
if (i) {
|
|
322
|
+
const y = [c, r, s, u, p].filter(Boolean).join(""), C = R(
|
|
323
|
+
g("sha256", this.apiSecret).update(y).digest()
|
|
324
|
+
);
|
|
325
|
+
Object.assign(l, {
|
|
326
|
+
"OK-ACCESS-KEY": this.apiKey,
|
|
327
|
+
"OK-ACCESS-TIMESTAMP": c,
|
|
328
|
+
"OK-ACCESS-PASSPHRASE": this.apiPass,
|
|
329
|
+
"OK-ACCESS-SIGN": C
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const m = `https://www.okx.com${s}`, f = k(m), h = await fetch(m + u, {
|
|
333
|
+
...T.options,
|
|
334
|
+
method: r,
|
|
335
|
+
headers: l,
|
|
336
|
+
body: p
|
|
337
|
+
}), S = await h.json();
|
|
338
|
+
if (f(), !h.ok || o && S.code !== "0")
|
|
339
|
+
throw { status: h.status, data: S };
|
|
340
|
+
return S.data;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
export {
|
|
344
|
+
P as BinanceRequester,
|
|
345
|
+
E as CDCRequester,
|
|
346
|
+
T as FetchConfig,
|
|
347
|
+
O as GeminiRequester,
|
|
348
|
+
I as OKXRequester,
|
|
349
|
+
G as buildSearchParams,
|
|
350
|
+
k as timeit
|
|
351
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IMarketType } from '@gera2ld/lib-trading';
|
|
2
|
+
import type { RequestOptions } from './types.ts';
|
|
3
|
+
export declare class BinanceRequester {
|
|
4
|
+
private apiKey;
|
|
5
|
+
private apiSecret;
|
|
6
|
+
marketType: IMarketType;
|
|
7
|
+
static defaultOpts: RequestOptions;
|
|
8
|
+
static create(marketType?: IMarketType): BinanceRequester;
|
|
9
|
+
private requestToken;
|
|
10
|
+
constructor(apiKey: string, apiSecret: string, marketType?: IMarketType);
|
|
11
|
+
private _request;
|
|
12
|
+
_requestWithRetry<T>(path: string, opts?: Partial<RequestOptions>): Promise<T>;
|
|
13
|
+
request<T = unknown>(path: string, opts?: Partial<RequestOptions>): Promise<T>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RequestOptions } from './types.ts';
|
|
2
|
+
export declare class CDCRequester {
|
|
3
|
+
private apiKey;
|
|
4
|
+
private apiSecret;
|
|
5
|
+
static defaultOpts: RequestOptions;
|
|
6
|
+
static create(subaccount?: string): CDCRequester;
|
|
7
|
+
requestId: number;
|
|
8
|
+
constructor(apiKey: string, apiSecret: string);
|
|
9
|
+
request<T = unknown>(path: string, opts?: Partial<RequestOptions>): Promise<{
|
|
10
|
+
code: number;
|
|
11
|
+
result: T;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RequestOptions } from './types.ts';
|
|
2
|
+
export declare class GeminiRequester {
|
|
3
|
+
private apiKey;
|
|
4
|
+
private apiSecret;
|
|
5
|
+
static defaultOpts: RequestOptions;
|
|
6
|
+
static create(subaccount?: string): GeminiRequester;
|
|
7
|
+
private requestToken;
|
|
8
|
+
constructor(apiKey: string, apiSecret: string);
|
|
9
|
+
private doRequest;
|
|
10
|
+
request<T = unknown>(path: string, opts?: Partial<RequestOptions>): Promise<T>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RequestOptions } from './types.ts';
|
|
2
|
+
export declare class OKXRequester {
|
|
3
|
+
private apiKey;
|
|
4
|
+
private apiSecret;
|
|
5
|
+
private apiPass;
|
|
6
|
+
static defaultOpts: RequestOptions;
|
|
7
|
+
static create(subaccount?: string): OKXRequester;
|
|
8
|
+
constructor(apiKey: string, apiSecret: string, apiPass: string);
|
|
9
|
+
request<T = unknown>(path: string, opts?: Partial<RequestOptions>): Promise<T>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const FetchConfig: {
|
|
2
|
+
/** Additional options passed to `fetch`. */
|
|
3
|
+
options?: any;
|
|
4
|
+
};
|
|
5
|
+
export declare function buildSearchParams(params: Record<string, unknown> | undefined): URLSearchParams;
|
|
6
|
+
export declare function timeit(label: string): () => void;
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gera2ld/lib-cex",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public",
|
|
16
|
+
"registry": "https://registry.npmjs.org/"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@gera2ld/common": "^0.0.1",
|
|
20
|
+
"@gera2ld/common-node": "^0.0.1",
|
|
21
|
+
"@gera2ld/lib-trading": "^0.0.1"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"es-toolkit": "^1.41.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"clean": "del-cli dist tsconfig.tsbuildinfo",
|
|
28
|
+
"build:types": "tsc",
|
|
29
|
+
"build:js": "vite build",
|
|
30
|
+
"build": "pnpm clean && pnpm /^build:/"
|
|
31
|
+
}
|
|
32
|
+
}
|