Skip to content

Commit 155cfc8

Browse files
committed
Transpile examples
1 parent 0ed46e8 commit 155cfc8

3 files changed

Lines changed: 360 additions & 2 deletions

File tree

build/rustTranspiler.ts

Lines changed: 220 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2155,8 +2155,8 @@ async fn dispatch(&mut self, method: Value, params: Value, context: Value) -> Va
21552155
class RustTranspiler {
21562156
sanitizeRustOutput(code: string) {
21572157
return code
2158-
.replace(/\.function toString\(\) \{ \[native code\] \}\(\)/g, '.to_string()')
2159-
.replace(/\bfunction toString\(\) \{ \[native code\] \}\(\)/g, 'to_string()')
2158+
.replace(/\.function\s+toString\(\)\s*\{\s*\[native code\]\s*\}\(\)/g, '.to_string()')
2159+
.replace(/\bfunction\s+toString\(\)\s*\{\s*\[native code\]\s*\}\(\)/g, 'to_string()')
21602160
.replace(/JSON\.into\(\)::parse/g, 'JSON::parse')
21612161
// Built-in call normalization pass (safe textual rewrites only).
21622162
.replace(/\.toString\(\)/g, '.to_string()')
@@ -2433,6 +2433,7 @@ class RustTranspiler {
24332433
const { allModules } = await this.transpileDerivedExchangeFiles('./js/src', rustExchanges, force);
24342434

24352435
this.exportRustModules('./rust/src/exchanges/mod.rs', allModules, ['binance']);
2436+
await this.transpileWs(force);
24362437

24372438
const libFile = './rust/src/lib.rs';
24382439
const libBody = [
@@ -2447,6 +2448,8 @@ class RustTranspiler {
24472448
].join('\n');
24482449
overwriteFile(libFile, libBody);
24492450

2451+
this.transpileExamples(force);
2452+
24502453
log.bright.green('Rust transpilation complete.');
24512454
}
24522455

@@ -2457,6 +2460,221 @@ class RustTranspiler {
24572460
const { allModules } = await this.transpileDerivedExchangeFiles('./js/src/pro', rustPro, force);
24582461
this.exportRustModules('./rust/src/pro/mod.rs', allModules);
24592462
}
2463+
2464+
getRustTraitNameFromExchangeId(exchangeId: string) {
2465+
return exchangeId.charAt(0).toUpperCase() + exchangeId.slice(1);
2466+
}
2467+
2468+
transpileExamples(force = false) {
2469+
const tsExamplesFolder = './examples/ts';
2470+
const rustExamplesFolder = './examples/rust';
2471+
createFolderRecursively(rustExamplesFolder);
2472+
2473+
if (!fs.existsSync(tsExamplesFolder)) {
2474+
return;
2475+
}
2476+
2477+
const generated: string[] = [];
2478+
2479+
const files = fs
2480+
.readdirSync(tsExamplesFolder)
2481+
.filter((f) => f.endsWith('.ts'))
2482+
.sort();
2483+
for (const file of files) {
2484+
const inputPath = path.join(tsExamplesFolder, file);
2485+
const outputName = path
2486+
.basename(file, '.ts')
2487+
.replace(/[^a-zA-Z0-9_]+/g, '_')
2488+
.replace(/^(\d)/, '_$1')
2489+
.toLowerCase();
2490+
const outputPath = path.join(rustExamplesFolder, `${outputName}.rs`);
2491+
2492+
const inMtime = fs.statSync(inputPath).mtime.getTime();
2493+
const outMtime = fs.existsSync(outputPath) ? fs.statSync(outputPath).mtime.getTime() : 0;
2494+
if (!force && inMtime <= outMtime) {
2495+
continue;
2496+
}
2497+
2498+
const tsCode = fs.readFileSync(inputPath, 'utf8');
2499+
2500+
const isPro = /new\s+ccxt\.pro\.[a-zA-Z0-9_]+\s*\(/.test(tsCode);
2501+
const exchangeMatch = /new\s+ccxt(?:\.pro)?\.([a-zA-Z0-9_]+)\s*\(/.exec(tsCode);
2502+
if (!exchangeMatch) {
2503+
const placeholder = [
2504+
'// AUTO-GENERATED: transpiled from TypeScript examples/',
2505+
`// Source: examples/ts/${file}`,
2506+
'',
2507+
'#[tokio::main]',
2508+
'async fn main() {',
2509+
` println!("No exchange constructor detected in ${file}; generated placeholder.");`,
2510+
'}',
2511+
'',
2512+
].join('\n');
2513+
overwriteFile(outputPath, placeholder);
2514+
fs.utimesSync(outputPath, new Date(), new Date(inMtime));
2515+
generated.push(outputName);
2516+
continue;
2517+
}
2518+
const exchangeId = exchangeMatch[1];
2519+
const exchangeModulePath = isPro ? `./rust/src/pro/${exchangeId}.rs` : `./rust/src/exchanges/${exchangeId}.rs`;
2520+
if (!fs.existsSync(exchangeModulePath)) {
2521+
const placeholder = [
2522+
'// AUTO-GENERATED: transpiled from TypeScript examples/',
2523+
`// Source: examples/ts/${file}`,
2524+
'',
2525+
'#[tokio::main]',
2526+
'async fn main() {',
2527+
` println!("No transpiled Rust module for exchange '${exchangeId}' (${isPro ? 'pro' : 'rest'}); generated placeholder.");`,
2528+
'}',
2529+
'',
2530+
].join('\n');
2531+
overwriteFile(outputPath, placeholder);
2532+
fs.utimesSync(outputPath, new Date(), new Date(inMtime));
2533+
generated.push(outputName);
2534+
continue;
2535+
}
2536+
let classNameKey = exchangeId;
2537+
try {
2538+
const sourcePath = isPro ? `./js/src/pro/${exchangeId}.js` : `./js/src/${exchangeId}.js`;
2539+
if (fs.existsSync(sourcePath)) {
2540+
const src = fs.readFileSync(sourcePath, 'utf8');
2541+
const classNode = getClassNode(src);
2542+
const className = classNode.id?.name;
2543+
if (className) {
2544+
classNameKey = className;
2545+
analyzeClassFromAst(className, classNode);
2546+
}
2547+
}
2548+
} catch (_) {
2549+
// Best-effort extraction only; keep fallback.
2550+
}
2551+
2552+
const methodNames = new Set<string>();
2553+
const exchangeVars = new Set<string>();
2554+
let m: RegExpExecArray | null = null;
2555+
const ctorVarRegex = /\b(?:const|let|var)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*new\s+ccxt(?:\.pro)?\.[a-zA-Z0-9_]+\s*\(/g;
2556+
while ((m = ctorVarRegex.exec(tsCode)) !== null) {
2557+
exchangeVars.add(m[1]);
2558+
}
2559+
if (exchangeVars.size === 0) {
2560+
exchangeVars.add('exchange');
2561+
}
2562+
const varsAlternation = Array.from(exchangeVars).join('|');
2563+
const methodRegex = new RegExp(`\\b(?:${varsAlternation})\\.([a-zA-Z][A-Za-z0-9_]*)\\s*\\(`, 'g');
2564+
while ((m = methodRegex.exec(tsCode)) !== null) {
2565+
methodNames.add(m[1]);
2566+
}
2567+
2568+
if (methodNames.size === 0) {
2569+
const placeholder = [
2570+
'// AUTO-GENERATED: transpiled from TypeScript examples/',
2571+
`// Source: examples/ts/${file}`,
2572+
'',
2573+
`use ${isPro ? `ccxt::pro::${exchangeId}` : `ccxt::exchanges::${exchangeId}`}::{${this.getRustTraitNameFromExchangeId(exchangeId)}Impl};`,
2574+
'use ccxt::exchange::Value;',
2575+
'use serde_json::json;',
2576+
'',
2577+
'#[tokio::main]',
2578+
'async fn main() {',
2579+
` let _exchange = ${this.getRustTraitNameFromExchangeId(exchangeId)}Impl::new(Value::Json(json!({})));`,
2580+
` println!("No exchange method calls detected in ${file}; generated placeholder.");`,
2581+
'}',
2582+
'',
2583+
].join('\n');
2584+
overwriteFile(outputPath, placeholder);
2585+
fs.utimesSync(outputPath, new Date(), new Date(inMtime));
2586+
generated.push(outputName);
2587+
continue;
2588+
}
2589+
2590+
const symbolMatch = /const\s+symbol\s*=\s*['"]([^'"]+)['"]/.exec(tsCode);
2591+
const symbol = symbolMatch?.[1] || 'BTC/USDT';
2592+
2593+
const traitName = this.getRustTraitNameFromExchangeId(exchangeId);
2594+
const importPath = isPro ? `ccxt::pro::${exchangeId}` : `ccxt::exchanges::${exchangeId}`;
2595+
const classFns = {
2596+
...(FUNCTION_INFO['Exchange'] || {}),
2597+
...(FUNCTION_INFO[classNameKey] || {}),
2598+
...(FUNCTION_INFO[traitName] || {}),
2599+
...(FUNCTION_INFO[exchangeId] || {}),
2600+
};
2601+
const exampleBody: string[] = [];
2602+
exampleBody.push('// AUTO-GENERATED: transpiled from TypeScript examples/');
2603+
exampleBody.push(`// Source: examples/ts/${file}`);
2604+
exampleBody.push('');
2605+
exampleBody.push('use ccxt::exchange::{normalize, Value};');
2606+
exampleBody.push(`use ${importPath}::{${traitName}, ${traitName}Impl};`);
2607+
exampleBody.push('use serde_json::json;');
2608+
exampleBody.push('');
2609+
exampleBody.push('#[tokio::main]');
2610+
exampleBody.push('async fn main() {');
2611+
exampleBody.push(` let mut exchange = ${traitName}Impl::new(Value::Json(json!({})));`);
2612+
exampleBody.push(` let symbol: Value = "${symbol}".into();`);
2613+
exampleBody.push('');
2614+
2615+
const orderedMethods = Array.from(methodNames).sort();
2616+
for (const method of orderedMethods) {
2617+
const fnInfo = classFns[method];
2618+
if (!fnInfo) {
2619+
exampleBody.push(` // skipped: ${method} (not found in transpiled trait)`);
2620+
continue;
2621+
}
2622+
const rustName = unCamelCase(method);
2623+
const args: string[] = [];
2624+
const needsSymbol =
2625+
/^(fetch|watch|create|cancel|edit|parse)/.test(method) ||
2626+
method.toLowerCase().includes('ticker') ||
2627+
method.toLowerCase().includes('orderbook') ||
2628+
method.toLowerCase().includes('ohlcv') ||
2629+
method.toLowerCase().includes('trade');
2630+
for (let i = 0; i < fnInfo.paramsCount; i++) {
2631+
if (i === 0 && needsSymbol) {
2632+
args.push('symbol.clone()');
2633+
} else {
2634+
args.push('Value::Undefined');
2635+
}
2636+
}
2637+
const argsExpr = args.length > 0 ? `, ${args.join(', ')}` : '';
2638+
if (fnInfo.async) {
2639+
exampleBody.push(` let rv = ${traitName}::${rustName}(&mut exchange${argsExpr}).await;`);
2640+
} else {
2641+
exampleBody.push(` let rv = ${traitName}::${rustName}(&mut exchange${argsExpr});`);
2642+
}
2643+
exampleBody.push(
2644+
` println!("${method}: {}", normalize(&rv).map(|v| v.to_string()).unwrap_or_else(|| "undefined".into()));`
2645+
);
2646+
}
2647+
2648+
exampleBody.push('}');
2649+
exampleBody.push('');
2650+
overwriteFile(outputPath, exampleBody.join('\n'));
2651+
fs.utimesSync(outputPath, new Date(), new Date(inMtime));
2652+
generated.push(outputName);
2653+
}
2654+
2655+
if (generated.length > 0) {
2656+
this.updateCargoExamples(generated);
2657+
log.cyan(`Transpiled Rust examples: ${generated.length}`);
2658+
}
2659+
}
2660+
2661+
updateCargoExamples(exampleNames: string[]) {
2662+
const cargoPath = './rust/Cargo.toml';
2663+
if (!fs.existsSync(cargoPath)) return;
2664+
const cargo = fs.readFileSync(cargoPath, 'utf8');
2665+
const startMarker = '# AUTO-GENERATED RUST EXAMPLES START';
2666+
const endMarker = '# AUTO-GENERATED RUST EXAMPLES END';
2667+
const block = [
2668+
startMarker,
2669+
...exampleNames
2670+
.sort()
2671+
.map((name) => `[[example]]\nname = "${name}"\npath = "../examples/rust/${name}.rs"`),
2672+
endMarker,
2673+
].join('\n\n');
2674+
const markerRegex = new RegExp(`${startMarker}[\\s\\S]*${endMarker}`, 'm');
2675+
const next = markerRegex.test(cargo) ? cargo.replace(markerRegex, block) : `${cargo.trimEnd()}\n\n${block}\n`;
2676+
overwriteFile(cargoPath, next);
2677+
}
24602678
}
24612679

24622680
if (process.argv[1] && process.argv[1].includes('rustTranspiler')) {

rust/Cargo.toml

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,127 @@ path = "src/bin/test.rs"
3939

4040
[profile.release]
4141
debug = true
42+
43+
# AUTO-GENERATED RUST EXAMPLES START
44+
45+
[[example]]
46+
name = "benchmark"
47+
path = "../examples/rust/benchmark.rs"
48+
49+
[[example]]
50+
name = "build_ohlcv_bars"
51+
path = "../examples/rust/build_ohlcv_bars.rs"
52+
53+
[[example]]
54+
name = "compare_two_exchanges_capabilities"
55+
path = "../examples/rust/compare_two_exchanges_capabilities.rs"
56+
57+
[[example]]
58+
name = "create_order_position_with_takeprofit_stoploss"
59+
path = "../examples/rust/create_order_position_with_takeprofit_stoploss.rs"
60+
61+
[[example]]
62+
name = "create_order_ws_example"
63+
path = "../examples/rust/create_order_ws_example.rs"
64+
65+
[[example]]
66+
name = "create_orders_example"
67+
path = "../examples/rust/create_orders_example.rs"
68+
69+
[[example]]
70+
name = "create_trailing_amount_order"
71+
path = "../examples/rust/create_trailing_amount_order.rs"
72+
73+
[[example]]
74+
name = "create_trailing_percent_order"
75+
path = "../examples/rust/create_trailing_percent_order.rs"
76+
77+
[[example]]
78+
name = "custom_proxy_agent_for_js"
79+
path = "../examples/rust/custom_proxy_agent_for_js.rs"
80+
81+
[[example]]
82+
name = "exchange_rate_limiter_rollingwindow"
83+
path = "../examples/rust/exchange_rate_limiter_rollingwindow.rs"
84+
85+
[[example]]
86+
name = "fetch_balance_all_exchanges"
87+
path = "../examples/rust/fetch_balance_all_exchanges.rs"
88+
89+
[[example]]
90+
name = "fetch_first_ohlcv_timestamp"
91+
path = "../examples/rust/fetch_first_ohlcv_timestamp.rs"
92+
93+
[[example]]
94+
name = "fetch_ohlcv"
95+
path = "../examples/rust/fetch_ohlcv.rs"
96+
97+
[[example]]
98+
name = "fetch_ohlcv_many_exchanges_continuosly"
99+
path = "../examples/rust/fetch_ohlcv_many_exchanges_continuosly.rs"
100+
101+
[[example]]
102+
name = "hibachi_example"
103+
path = "../examples/rust/hibachi_example.rs"
104+
105+
[[example]]
106+
name = "how_to_import_one_exchange_esm"
107+
path = "../examples/rust/how_to_import_one_exchange_esm.rs"
108+
109+
[[example]]
110+
name = "hyperliquid_load_hip3_dexes"
111+
path = "../examples/rust/hyperliquid_load_hip3_dexes.rs"
112+
113+
[[example]]
114+
name = "kraken_create_and_close_position"
115+
path = "../examples/rust/kraken_create_and_close_position.rs"
116+
117+
[[example]]
118+
name = "margin_loan_borrow_buy_sell_repay"
119+
path = "../examples/rust/margin_loan_borrow_buy_sell_repay.rs"
120+
121+
[[example]]
122+
name = "phemex_create_order_position_with_takeprofit_stoploss"
123+
path = "../examples/rust/phemex_create_order_position_with_takeprofit_stoploss.rs"
124+
125+
[[example]]
126+
name = "proxy_usage"
127+
path = "../examples/rust/proxy_usage.rs"
128+
129+
[[example]]
130+
name = "sample_local_proxy_server_with_cors"
131+
path = "../examples/rust/sample_local_proxy_server_with_cors.rs"
132+
133+
[[example]]
134+
name = "watch_ohlcv"
135+
path = "../examples/rust/watch_ohlcv.rs"
136+
137+
[[example]]
138+
name = "watch_ohlcv_for_symbols"
139+
path = "../examples/rust/watch_ohlcv_for_symbols.rs"
140+
141+
[[example]]
142+
name = "watch_orderbook_for_symbols"
143+
path = "../examples/rust/watch_orderbook_for_symbols.rs"
144+
145+
[[example]]
146+
name = "watch_tickers"
147+
path = "../examples/rust/watch_tickers.rs"
148+
149+
[[example]]
150+
name = "watch_trades_for_symbols"
151+
path = "../examples/rust/watch_trades_for_symbols.rs"
152+
153+
[[example]]
154+
name = "watchpositions"
155+
path = "../examples/rust/watchpositions.rs"
156+
157+
[[example]]
158+
name = "watchpositions_many_exchanges_continuosly"
159+
path = "../examples/rust/watchpositions_many_exchanges_continuosly.rs"
160+
161+
[[example]]
162+
name = "watchpositionsforsymbols"
163+
path = "../examples/rust/watchpositionsforsymbols.rs"
164+
165+
# AUTO-GENERATED RUST EXAMPLES END

0 commit comments

Comments
 (0)