@solard/cli 0.2.13 → 0.2.14
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/package.json +2 -2
- package/src/backtest-batch-command.ts +262 -0
- package/src/backtest-command.ts +533 -0
- package/src/commands/buy.ts +207 -0
- package/src/commands/jupiter-swap.test.ts +78 -0
- package/src/commands/jupiter-swap.ts +269 -0
- package/src/commands/sell.ts +239 -0
- package/src/commands/trade-result.ts +19 -0
- package/src/commands/trade-targets.ts +43 -0
- package/src/commands/trade-venue.test.ts +32 -0
- package/src/commands/trade-venue.ts +19 -0
- package/src/commands/transfer.ts +200 -0
- package/src/fairfun-commands.ts +381 -0
- package/src/index.ts +386 -290
- package/src/meteora-commands.ts +148 -6
- package/src/pump/external-deployment-cli.ts +136 -0
- package/src/pump/token-launch-cli.ts +82 -6
- package/src/raydium-commands.ts +398 -0
- package/src/refs.ts +56 -0
- package/src/token-history-command.ts +559 -0
- package/src/vault-onboarding.ts +214 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
import { Keypair, PublicKey } from "@solana/web3.js";
|
|
10
|
+
import {
|
|
11
|
+
RaydiumService,
|
|
12
|
+
formatRaw,
|
|
13
|
+
resolveTradeAsset,
|
|
14
|
+
sol,
|
|
15
|
+
tokenAmount,
|
|
16
|
+
type Solard,
|
|
17
|
+
type TradeAsset,
|
|
18
|
+
} from "@solard/sdk";
|
|
19
|
+
|
|
20
|
+
type Flags = Map<string, string>;
|
|
21
|
+
type Emit = (value: string) => void;
|
|
22
|
+
|
|
23
|
+
type Args = {
|
|
24
|
+
slrd: Solard;
|
|
25
|
+
values: string[];
|
|
26
|
+
flags: Flags;
|
|
27
|
+
emit: Emit;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function json(value: unknown): string {
|
|
31
|
+
return JSON.stringify(
|
|
32
|
+
value,
|
|
33
|
+
(_, item) => (typeof item === "bigint" ? item.toString() : item),
|
|
34
|
+
2,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function flag(flags: Flags, key: string): string | undefined {
|
|
39
|
+
const value = flags.get(key);
|
|
40
|
+
return value && value !== "true" ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function need(flags: Flags, key: string): string {
|
|
44
|
+
const value = flag(flags, key);
|
|
45
|
+
if (!value) throw new Error(`Missing --${key} <value>`);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function intFlag(flags: Flags, key: string, fallback: number): number {
|
|
50
|
+
const value = flag(flags, key);
|
|
51
|
+
if (value == null) return fallback;
|
|
52
|
+
const parsed = Number(value);
|
|
53
|
+
if (!Number.isInteger(parsed)) throw new Error(`--${key} must be an integer`);
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function numberFlag(flags: Flags, key: string, fallback: number): number {
|
|
58
|
+
const value = flag(flags, key);
|
|
59
|
+
if (value == null) return fallback;
|
|
60
|
+
const parsed = Number(value);
|
|
61
|
+
if (!Number.isFinite(parsed)) throw new Error(`--${key} must be a number`);
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function rawFor(asset: TradeAsset, amountUi: string): bigint {
|
|
66
|
+
if (asset.kind !== "sol" && !asset.tokenProgram) {
|
|
67
|
+
throw new Error(`Missing token program for ${asset.mint}`);
|
|
68
|
+
}
|
|
69
|
+
const raw =
|
|
70
|
+
asset.kind === "sol"
|
|
71
|
+
? sol(amountUi).raw
|
|
72
|
+
: tokenAmount(
|
|
73
|
+
amountUi,
|
|
74
|
+
new PublicKey(asset.mint),
|
|
75
|
+
asset.decimals,
|
|
76
|
+
new PublicKey(asset.tokenProgram!),
|
|
77
|
+
).raw;
|
|
78
|
+
if (raw <= 0n) throw new Error("Amount must be greater than zero");
|
|
79
|
+
return raw;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function assets(
|
|
83
|
+
slrd: Solard,
|
|
84
|
+
from: string,
|
|
85
|
+
to: string,
|
|
86
|
+
): Promise<[TradeAsset, TradeAsset]> {
|
|
87
|
+
return await Promise.all([
|
|
88
|
+
resolveTradeAsset(slrd, from),
|
|
89
|
+
resolveTradeAsset(slrd, to),
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function executionOptions(flags: Flags) {
|
|
94
|
+
return {
|
|
95
|
+
live: flags.has("live"),
|
|
96
|
+
simulate: !flags.has("skip-simulation"),
|
|
97
|
+
skipPreflight: flags.has("skip-preflight"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readKeypair(path: string): Keypair {
|
|
102
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
103
|
+
if (
|
|
104
|
+
!Array.isArray(parsed) ||
|
|
105
|
+
!parsed.every((value) => Number.isInteger(value))
|
|
106
|
+
) {
|
|
107
|
+
throw new Error(`Invalid Solana keypair JSON: ${path}`);
|
|
108
|
+
}
|
|
109
|
+
return Keypair.fromSecretKey(Uint8Array.from(parsed));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function saveKeypair(path: string, signer: Keypair): string {
|
|
113
|
+
const absolute = resolve(path);
|
|
114
|
+
mkdirSync(dirname(absolute), { recursive: true });
|
|
115
|
+
writeFileSync(absolute, `${JSON.stringify([...signer.secretKey])}\n`, {
|
|
116
|
+
flag: "wx",
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
chmodSync(absolute, 0o600);
|
|
120
|
+
} catch {}
|
|
121
|
+
return absolute;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function launchMintSigner(flags: Flags): { signer: Keypair; path: string } {
|
|
125
|
+
const requested = flag(flags, "mint-keypair");
|
|
126
|
+
if (requested && existsSync(resolve(requested))) {
|
|
127
|
+
return {
|
|
128
|
+
signer: readKeypair(resolve(requested)),
|
|
129
|
+
path: resolve(requested),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const signer = Keypair.generate();
|
|
133
|
+
const path = requested
|
|
134
|
+
? resolve(requested)
|
|
135
|
+
: resolve(
|
|
136
|
+
".solard",
|
|
137
|
+
"raydium-mints",
|
|
138
|
+
`${signer.publicKey.toBase58()}.json`,
|
|
139
|
+
);
|
|
140
|
+
return { signer, path: saveKeypair(path, signer) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function help(): string {
|
|
144
|
+
return `Raydium
|
|
145
|
+
slrd raydium quote --from <SOL|token|mint> --to <SOL|token|mint> --amount <ui> [--slippage-bps 100]
|
|
146
|
+
slrd raydium swap --from <SOL|token|mint> --to <SOL|token|mint> --amount <ui> --wallet <wallet> [--live]
|
|
147
|
+
|
|
148
|
+
LaunchLab
|
|
149
|
+
slrd raydium launchlab configs [--quote <SOL|token|mint>]
|
|
150
|
+
slrd raydium launchlab launch --wallet <wallet> --name <name> --symbol <symbol> --uri <metadata-uri> [--quote SOL|mint] [--buy <ui>] [--decimals 6] [--mint-keypair <path>] [--live]
|
|
151
|
+
slrd raydium launchlab buy <mint> --wallet <wallet> [--quote SOL|mint] --amount <ui> [--live]
|
|
152
|
+
slrd raydium launchlab sell <mint> --wallet <wallet> [--quote SOL|mint] --amount <ui> [--live]
|
|
153
|
+
|
|
154
|
+
Arbitrary SPL/SPL pair
|
|
155
|
+
slrd raydium cpmm create --wallet <wallet> --mint-a <token|mint> --mint-b <token|mint> --amount-a <ui> --amount-b <ui> [--fee-config-index 0] [--live]
|
|
156
|
+
|
|
157
|
+
All writes are simulation-only unless --live is supplied. Live Raydium writes also require SOLARD_ENABLE_LIVE_TRADES=1.`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function handleRaydiumCommand({
|
|
161
|
+
slrd,
|
|
162
|
+
values,
|
|
163
|
+
flags,
|
|
164
|
+
emit,
|
|
165
|
+
}: Args): Promise<void> {
|
|
166
|
+
const service = new RaydiumService(slrd);
|
|
167
|
+
const section = values[0] ?? "help";
|
|
168
|
+
|
|
169
|
+
if (section === "help" || flags.has("help")) {
|
|
170
|
+
emit(`${help()}\n`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (section === "quote") {
|
|
175
|
+
const inputRef = need(flags, "from");
|
|
176
|
+
const outputRef = need(flags, "to");
|
|
177
|
+
const amountUi = need(flags, "amount");
|
|
178
|
+
const [input, output] = await assets(slrd, inputRef, outputRef);
|
|
179
|
+
const quote = await service.quoteExactIn({
|
|
180
|
+
inputMint: input.mint,
|
|
181
|
+
outputMint: output.mint,
|
|
182
|
+
amountRaw: rawFor(input, amountUi),
|
|
183
|
+
slippageBps: intFlag(flags, "slippage-bps", 100),
|
|
184
|
+
});
|
|
185
|
+
emit(
|
|
186
|
+
`${json({
|
|
187
|
+
mode: "quote",
|
|
188
|
+
venue: "raydium",
|
|
189
|
+
input: { ...input, amountUi, amountRaw: quote.inputRaw },
|
|
190
|
+
output: {
|
|
191
|
+
...output,
|
|
192
|
+
expectedOutputUi: formatRaw(quote.outputRaw, output.decimals),
|
|
193
|
+
minimumOutputUi: formatRaw(quote.minOutputRaw, output.decimals),
|
|
194
|
+
},
|
|
195
|
+
quote,
|
|
196
|
+
})}\n`,
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (section === "swap") {
|
|
202
|
+
const wallet = need(flags, "wallet");
|
|
203
|
+
const inputRef = need(flags, "from");
|
|
204
|
+
const outputRef = need(flags, "to");
|
|
205
|
+
const amountUi = need(flags, "amount");
|
|
206
|
+
const [input, output] = await assets(slrd, inputRef, outputRef);
|
|
207
|
+
const prepared = await service.buildSwapExactIn({
|
|
208
|
+
wallet,
|
|
209
|
+
inputMint: input.mint,
|
|
210
|
+
outputMint: output.mint,
|
|
211
|
+
amountRaw: rawFor(input, amountUi),
|
|
212
|
+
slippageBps: intFlag(flags, "slippage-bps", 100),
|
|
213
|
+
computeUnitPriceMicroLamports:
|
|
214
|
+
flag(flags, "priority-micro-lamports") ?? undefined,
|
|
215
|
+
});
|
|
216
|
+
const result = await service.executePrepared(
|
|
217
|
+
prepared,
|
|
218
|
+
executionOptions(flags),
|
|
219
|
+
);
|
|
220
|
+
emit(
|
|
221
|
+
`${json({
|
|
222
|
+
mode: flags.has("live") ? "live" : "simulation",
|
|
223
|
+
venue: "raydium",
|
|
224
|
+
input: { ...input, amountUi },
|
|
225
|
+
output: {
|
|
226
|
+
...output,
|
|
227
|
+
expectedOutputUi: formatRaw(
|
|
228
|
+
prepared.quote.outputRaw,
|
|
229
|
+
output.decimals,
|
|
230
|
+
),
|
|
231
|
+
minimumOutputUi: formatRaw(
|
|
232
|
+
prepared.quote.minOutputRaw,
|
|
233
|
+
output.decimals,
|
|
234
|
+
),
|
|
235
|
+
},
|
|
236
|
+
result,
|
|
237
|
+
})}\n`,
|
|
238
|
+
);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (section === "launchlab") {
|
|
243
|
+
const action = values[1] ?? "help";
|
|
244
|
+
|
|
245
|
+
if (action === "help") {
|
|
246
|
+
emit(`${help()}\n`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (action === "configs") {
|
|
251
|
+
const quoteRef = flag(flags, "quote");
|
|
252
|
+
const quote = quoteRef ? await resolveTradeAsset(slrd, quoteRef) : null;
|
|
253
|
+
let configs = await service.listLaunchConfigs();
|
|
254
|
+
if (quote) configs = configs.filter((row) => row.mintB === quote.mint);
|
|
255
|
+
emit(`${json({ quote, configs })}\n`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (action === "launch" || action === "create") {
|
|
260
|
+
const wallet = need(flags, "wallet");
|
|
261
|
+
const name = need(flags, "name");
|
|
262
|
+
const symbol = need(flags, "symbol");
|
|
263
|
+
const uri = need(flags, "uri");
|
|
264
|
+
const quote = await resolveTradeAsset(
|
|
265
|
+
slrd,
|
|
266
|
+
flag(flags, "quote") ?? "SOL",
|
|
267
|
+
);
|
|
268
|
+
const buyUi = flag(flags, "buy") ?? "0";
|
|
269
|
+
const buyRaw = buyUi === "0" ? 0n : rawFor(quote, buyUi);
|
|
270
|
+
// Fail before generating/saving a mint keypair when this quote is not
|
|
271
|
+
// actually supported by an on-chain LaunchLab config.
|
|
272
|
+
await service.resolveLaunchConfig({
|
|
273
|
+
quoteMint: quote.mint,
|
|
274
|
+
configId: flag(flags, "config"),
|
|
275
|
+
});
|
|
276
|
+
const mint = launchMintSigner(flags);
|
|
277
|
+
const prepared = await service.buildLaunchLabCreate({
|
|
278
|
+
wallet,
|
|
279
|
+
mintSigner: mint.signer,
|
|
280
|
+
name,
|
|
281
|
+
symbol,
|
|
282
|
+
uri,
|
|
283
|
+
quoteMint: quote.mint,
|
|
284
|
+
buyAmountRaw: buyRaw,
|
|
285
|
+
decimals: intFlag(flags, "decimals", 6),
|
|
286
|
+
slippageBps: intFlag(flags, "slippage-bps", 100),
|
|
287
|
+
configId: flag(flags, "config"),
|
|
288
|
+
token2022: flags.has("token-2022"),
|
|
289
|
+
});
|
|
290
|
+
const result = await service.executePrepared(
|
|
291
|
+
prepared,
|
|
292
|
+
executionOptions(flags),
|
|
293
|
+
);
|
|
294
|
+
emit(
|
|
295
|
+
`${json({
|
|
296
|
+
mode: flags.has("live") ? "live" : "simulation",
|
|
297
|
+
venue: "raydium-launchlab",
|
|
298
|
+
mint: mint.signer.publicKey.toBase58(),
|
|
299
|
+
mintKeypairPath: mint.path,
|
|
300
|
+
quote,
|
|
301
|
+
result,
|
|
302
|
+
hint: flags.has("live")
|
|
303
|
+
? null
|
|
304
|
+
: `Re-run with --mint-keypair ${mint.path} --live to launch this exact mint.`,
|
|
305
|
+
})}\n`,
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (action === "buy" || action === "sell") {
|
|
311
|
+
const mintRef = values[2];
|
|
312
|
+
if (!mintRef) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Usage: slrd raydium launchlab ${action} <mint> --wallet <wallet> [--quote SOL|mint] --amount <ui> [--live]`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const wallet = need(flags, "wallet");
|
|
318
|
+
const amountUi = need(flags, "amount");
|
|
319
|
+
const mint = await resolveTradeAsset(slrd, mintRef);
|
|
320
|
+
const quote = await resolveTradeAsset(
|
|
321
|
+
slrd,
|
|
322
|
+
flag(flags, "quote") ?? "SOL",
|
|
323
|
+
);
|
|
324
|
+
const prepared =
|
|
325
|
+
action === "buy"
|
|
326
|
+
? await service.buildLaunchLabBuy({
|
|
327
|
+
wallet,
|
|
328
|
+
mint: mint.mint,
|
|
329
|
+
quoteMint: quote.mint,
|
|
330
|
+
amountRaw: rawFor(quote, amountUi),
|
|
331
|
+
slippageBps: intFlag(flags, "slippage-bps", 100),
|
|
332
|
+
})
|
|
333
|
+
: await service.buildLaunchLabSell({
|
|
334
|
+
wallet,
|
|
335
|
+
mint: mint.mint,
|
|
336
|
+
quoteMint: quote.mint,
|
|
337
|
+
amountRaw: rawFor(mint, amountUi),
|
|
338
|
+
slippageBps: intFlag(flags, "slippage-bps", 100),
|
|
339
|
+
});
|
|
340
|
+
const result = await service.executePrepared(
|
|
341
|
+
prepared,
|
|
342
|
+
executionOptions(flags),
|
|
343
|
+
);
|
|
344
|
+
emit(
|
|
345
|
+
`${json({
|
|
346
|
+
mode: flags.has("live") ? "live" : "simulation",
|
|
347
|
+
venue: "raydium-launchlab",
|
|
348
|
+
side: action,
|
|
349
|
+
mint,
|
|
350
|
+
quote,
|
|
351
|
+
amountUi,
|
|
352
|
+
result,
|
|
353
|
+
})}\n`,
|
|
354
|
+
);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
throw new Error(`Unknown Raydium LaunchLab action: ${action}\n${help()}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (section === "cpmm") {
|
|
362
|
+
const action = values[1] ?? "help";
|
|
363
|
+
if (action !== "create") {
|
|
364
|
+
emit(`${help()}\n`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const wallet = need(flags, "wallet");
|
|
368
|
+
const mintA = await resolveTradeAsset(slrd, need(flags, "mint-a"));
|
|
369
|
+
const mintB = await resolveTradeAsset(slrd, need(flags, "mint-b"));
|
|
370
|
+
const amountAUi = need(flags, "amount-a");
|
|
371
|
+
const amountBUi = need(flags, "amount-b");
|
|
372
|
+
const prepared = await service.buildCpmmCreate({
|
|
373
|
+
wallet,
|
|
374
|
+
mintA: mintA.mint,
|
|
375
|
+
mintB: mintB.mint,
|
|
376
|
+
amountARaw: rawFor(mintA, amountAUi),
|
|
377
|
+
amountBRaw: rawFor(mintB, amountBUi),
|
|
378
|
+
feeConfigIndex: intFlag(flags, "fee-config-index", 0),
|
|
379
|
+
startTime: numberFlag(flags, "start-time", 0),
|
|
380
|
+
});
|
|
381
|
+
const result = await service.executePrepared(
|
|
382
|
+
prepared,
|
|
383
|
+
executionOptions(flags),
|
|
384
|
+
);
|
|
385
|
+
emit(
|
|
386
|
+
`${json({
|
|
387
|
+
mode: flags.has("live") ? "live" : "simulation",
|
|
388
|
+
venue: "raydium-cpmm",
|
|
389
|
+
mintA: { ...mintA, amountUi: amountAUi },
|
|
390
|
+
mintB: { ...mintB, amountUi: amountBUi },
|
|
391
|
+
result,
|
|
392
|
+
})}\n`,
|
|
393
|
+
);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
throw new Error(`Unknown Raydium command: ${section}\n${help()}`);
|
|
398
|
+
}
|
package/src/refs.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { findExternalContact } from "@solard/sdk";
|
|
2
|
+
|
|
3
|
+
export type ResolvedDestinationRef = {
|
|
4
|
+
input: string;
|
|
5
|
+
address: string;
|
|
6
|
+
contactName?: string;
|
|
7
|
+
walletName?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type WalletResolver = {
|
|
11
|
+
resolveWallet(ref: string): { address: { toBase58(): string } };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a CLI destination without ever treating an external contact as a
|
|
16
|
+
* signing wallet. Ambiguous contact/wallet names fail closed.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveDestinationRef(
|
|
19
|
+
slrd: WalletResolver,
|
|
20
|
+
value: string,
|
|
21
|
+
): ResolvedDestinationRef {
|
|
22
|
+
const input = value.trim();
|
|
23
|
+
const contact = findExternalContact(input);
|
|
24
|
+
|
|
25
|
+
let walletAddress: string | null = null;
|
|
26
|
+
try {
|
|
27
|
+
walletAddress = slrd.resolveWallet(input).address.toBase58();
|
|
28
|
+
} catch {
|
|
29
|
+
walletAddress = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (contact && walletAddress && contact.address !== walletAddress) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Ambiguous destination ${input}: external contact @${contact.name} points to ${contact.address}, ` +
|
|
35
|
+
`but a stored signing wallet resolves to ${walletAddress}. Rename one of them.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (contact) {
|
|
40
|
+
return {
|
|
41
|
+
input,
|
|
42
|
+
address: contact.address,
|
|
43
|
+
contactName: contact.name,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (walletAddress) {
|
|
48
|
+
return {
|
|
49
|
+
input,
|
|
50
|
+
address: walletAddress,
|
|
51
|
+
walletName: input.replace(/^@/, ""),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { input, address: input };
|
|
56
|
+
}
|