UNPKG

@btr-supply/swap-cli

Version:

Command-line interface for the BTR Swap SDK

639 lines (628 loc) 22.9 kB
#!/usr/bin/env node import { createRequire } from "node:module"; var __create = Object.create; var __getProtoOf = Object.getPrototypeOf; var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __toESM = (mod, isNodeMode, target) => { target = mod != null ? __create(__getProtoOf(mod)) : {}; const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; for (let key of __getOwnPropNames(mod)) if (!__hasOwnProp.call(to, key)) __defProp(to, key, { get: () => mod[key], enumerable: true }); return to; }; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); var __require = /* @__PURE__ */ createRequire(import.meta.url); // node_modules/dotenv/package.json var require_package = __commonJS((exports, module) => { module.exports = { name: "dotenv", version: "16.5.0", description: "Loads environment variables from .env file", main: "lib/main.js", types: "lib/main.d.ts", exports: { ".": { types: "./lib/main.d.ts", require: "./lib/main.js", default: "./lib/main.js" }, "./config": "./config.js", "./config.js": "./config.js", "./lib/env-options": "./lib/env-options.js", "./lib/env-options.js": "./lib/env-options.js", "./lib/cli-options": "./lib/cli-options.js", "./lib/cli-options.js": "./lib/cli-options.js", "./package.json": "./package.json" }, scripts: { "dts-check": "tsc --project tests/types/tsconfig.json", lint: "standard", pretest: "npm run lint && npm run dts-check", test: "tap run --allow-empty-coverage --disable-coverage --timeout=60000", "test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=lcov", prerelease: "npm test", release: "standard-version" }, repository: { type: "git", url: "git://github.com/motdotla/dotenv.git" }, homepage: "https://github.com/motdotla/dotenv#readme", funding: "https://dotenvx.com", keywords: [ "dotenv", "env", ".env", "environment", "variables", "config", "settings" ], readmeFilename: "README.md", license: "BSD-2-Clause", devDependencies: { "@types/node": "^18.11.3", decache: "^4.6.2", sinon: "^14.0.1", standard: "^17.0.0", "standard-version": "^9.5.0", tap: "^19.2.0", typescript: "^4.8.4" }, engines: { node: ">=12" }, browser: { fs: false } }; }); // node_modules/dotenv/lib/main.js var require_main = __commonJS((exports, module) => { var fs = __require("fs"); var path = __require("path"); var os = __require("os"); var crypto = __require("crypto"); var packageJson = require_package(); var version = packageJson.version; var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg; function parse(src) { const obj = {}; let lines = src.toString(); lines = lines.replace(/\r\n?/mg, ` `); let match; while ((match = LINE.exec(lines)) != null) { const key = match[1]; let value = match[2] || ""; value = value.trim(); const maybeQuote = value[0]; value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2"); if (maybeQuote === '"') { value = value.replace(/\\n/g, ` `); value = value.replace(/\\r/g, "\r"); } obj[key] = value; } return obj; } function _parseVault(options) { const vaultPath = _vaultPath(options); const result = DotenvModule.configDotenv({ path: vaultPath }); if (!result.parsed) { const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`); err.code = "MISSING_DATA"; throw err; } const keys = _dotenvKey(options).split(","); const length = keys.length; let decrypted; for (let i = 0;i < length; i++) { try { const key = keys[i].trim(); const attrs = _instructions(result, key); decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key); break; } catch (error) { if (i + 1 >= length) { throw error; } } } return DotenvModule.parse(decrypted); } function _warn(message) { console.log(`[dotenv@${version}][WARN] ${message}`); } function _debug(message) { console.log(`[dotenv@${version}][DEBUG] ${message}`); } function _dotenvKey(options) { if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) { return options.DOTENV_KEY; } if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) { return process.env.DOTENV_KEY; } return ""; } function _instructions(result, dotenvKey) { let uri; try { uri = new URL(dotenvKey); } catch (error) { if (error.code === "ERR_INVALID_URL") { const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development"); err.code = "INVALID_DOTENV_KEY"; throw err; } throw error; } const key = uri.password; if (!key) { const err = new Error("INVALID_DOTENV_KEY: Missing key part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environment = uri.searchParams.get("environment"); if (!environment) { const err = new Error("INVALID_DOTENV_KEY: Missing environment part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`; const ciphertext = result.parsed[environmentKey]; if (!ciphertext) { const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`); err.code = "NOT_FOUND_DOTENV_ENVIRONMENT"; throw err; } return { ciphertext, key }; } function _vaultPath(options) { let possibleVaultPath = null; if (options && options.path && options.path.length > 0) { if (Array.isArray(options.path)) { for (const filepath of options.path) { if (fs.existsSync(filepath)) { possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`; } } } else { possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`; } } else { possibleVaultPath = path.resolve(process.cwd(), ".env.vault"); } if (fs.existsSync(possibleVaultPath)) { return possibleVaultPath; } return null; } function _resolveHome(envPath) { return envPath[0] === "~" ? path.join(os.homedir(), envPath.slice(1)) : envPath; } function _configVault(options) { const debug = Boolean(options && options.debug); if (debug) { _debug("Loading env from encrypted .env.vault"); } const parsed = DotenvModule._parseVault(options); let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsed, options); return { parsed }; } function configDotenv(options) { const dotenvPath = path.resolve(process.cwd(), ".env"); let encoding = "utf8"; const debug = Boolean(options && options.debug); if (options && options.encoding) { encoding = options.encoding; } else { if (debug) { _debug("No encoding is specified. UTF-8 is used by default"); } } let optionPaths = [dotenvPath]; if (options && options.path) { if (!Array.isArray(options.path)) { optionPaths = [_resolveHome(options.path)]; } else { optionPaths = []; for (const filepath of options.path) { optionPaths.push(_resolveHome(filepath)); } } } let lastError; const parsedAll = {}; for (const path2 of optionPaths) { try { const parsed = DotenvModule.parse(fs.readFileSync(path2, { encoding })); DotenvModule.populate(parsedAll, parsed, options); } catch (e) { if (debug) { _debug(`Failed to load ${path2} ${e.message}`); } lastError = e; } } let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsedAll, options); if (lastError) { return { parsed: parsedAll, error: lastError }; } else { return { parsed: parsedAll }; } } function config(options) { if (_dotenvKey(options).length === 0) { return DotenvModule.configDotenv(options); } const vaultPath = _vaultPath(options); if (!vaultPath) { _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`); return DotenvModule.configDotenv(options); } return DotenvModule._configVault(options); } function decrypt(encrypted, keyStr) { const key = Buffer.from(keyStr.slice(-64), "hex"); let ciphertext = Buffer.from(encrypted, "base64"); const nonce = ciphertext.subarray(0, 12); const authTag = ciphertext.subarray(-16); ciphertext = ciphertext.subarray(12, -16); try { const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce); aesgcm.setAuthTag(authTag); return `${aesgcm.update(ciphertext)}${aesgcm.final()}`; } catch (error) { const isRange = error instanceof RangeError; const invalidKeyLength = error.message === "Invalid key length"; const decryptionFailed = error.message === "Unsupported state or unable to authenticate data"; if (isRange || invalidKeyLength) { const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)"); err.code = "INVALID_DOTENV_KEY"; throw err; } else if (decryptionFailed) { const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY"); err.code = "DECRYPTION_FAILED"; throw err; } else { throw error; } } } function populate(processEnv, parsed, options = {}) { const debug = Boolean(options && options.debug); const override = Boolean(options && options.override); if (typeof parsed !== "object") { const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate"); err.code = "OBJECT_REQUIRED"; throw err; } for (const key of Object.keys(parsed)) { if (Object.prototype.hasOwnProperty.call(processEnv, key)) { if (override === true) { processEnv[key] = parsed[key]; } if (debug) { if (override === true) { _debug(`"${key}" is already defined and WAS overwritten`); } else { _debug(`"${key}" is already defined and was NOT overwritten`); } } } else { processEnv[key] = parsed[key]; } } } var DotenvModule = { configDotenv, _configVault, _parseVault, config, decrypt, parse, populate }; exports.configDotenv = DotenvModule.configDotenv; exports._configVault = DotenvModule._configVault; exports._parseVault = DotenvModule._parseVault; exports.config = DotenvModule.config; exports.decrypt = DotenvModule.decrypt; exports.parse = DotenvModule.parse; exports.populate = DotenvModule.populate; module.exports = DotenvModule; }); // packages/cli/src/cli.ts import { readFileSync } from "fs"; import { AggId as AggId2, defaultAggregators, DisplayMode as DisplayMode2, getAllTimedTr, getToken, MAX_SLIPPAGE_BPS, SerializationMode as SerializationMode3, toJSON as toJSON2 } from "@btr-supply/swap"; // packages/cli/src/utils.ts var import_dotenv = __toESM(require_main(), 1); import { compactTrs, config as coreConfig, DisplayMode, getPerformance, getPerformanceTable, SerializationMode, serialize } from "@btr-supply/swap"; import * as fs from "fs"; import path from "path"; var handleError = (msg) => (console.error("❌", msg), process.exit(1)); var parseArgs = (args) => { const p = { _command: args[0] === "quote" ? "quote" : undefined }; const toCamelCase = (s) => s.replace(/-(.)/g, (_, c) => c.toUpperCase()); let verbose_level = 0; for (let i = 0;i < args.length; i++) { const a = args[i]; if (a === "-h" || a === "--help") p.help = true; else if (a === "--version") p.version = true; else if (a === "-vv") verbose_level += 2; else if (a === "-v" || a === "--verbose") verbose_level += 1; else if (a.startsWith("--")) { const key = toCamelCase(a.slice(2)); if (key === "verbose") {} else { p[key] = args[i + 1] && !args[i + 1].startsWith("-") ? args[++i] : true; } } } if (verbose_level > 0) { p.verbose = verbose_level; } return p; }; function parseEnumArg(val, Enum, def, multi = false) { const selections = String(val ?? "").split(",").filter(Boolean).map((s) => s.toUpperCase()); const choices = selections.map((v) => Object.values(Enum).find((e) => String(e).toUpperCase() === v)).filter((x) => x != null); return multi ? choices.length ? choices : def : choices[0] ?? def; } var parseJson = (key, args) => args[key] ? JSON.parse(args[key]) : undefined; function loadEnv(envPath) { try { const dotenvPath = envPath ? path.resolve(envPath) : path.resolve(process.cwd(), ".env"); if (!fs.existsSync(dotenvPath)) { return; } const result = import_dotenv.config({ path: dotenvPath, override: true }); return result.parsed && Object.keys(result.parsed).length > 0 ? result.parsed : undefined; } catch (error) { console.error(`Error loading env file: ${error instanceof Error ? error.message : String(error)}`); return; } } var applyConfig = (cli, silent) => Object.entries(coreConfig).forEach(([id, cfg]) => { const agg = id; const ek = process.env[`${agg}_API_KEY`]; if (ek) cfg.apiKey = ek; const er = process.env[`${agg}_REFERRER`]; if (er !== undefined) { const num = Number(er); cfg.referrer = isNaN(num) ? er : num; } const ei = process.env[`${agg}_INTEGRATOR`]; if (ei) cfg.integrator = ei; const ef = process.env[`${agg}_FEE_BPS`]; if (ef !== undefined) { const num = parseInt(ef, 10); if (!isNaN(num)) cfg.feeBps = num; else if (!silent) console.warn(`⚠️ Invalid fee BPS for ${agg}: ${ef}`); } if (cli.apiKeys?.[agg]) cfg.apiKey = cli.apiKeys[agg]; if (cli.referrer?.[agg] !== undefined) cfg.referrer = cli.referrer[agg]; if (cli.integrators?.[agg]) cfg.integrator = cli.integrators[agg]; if (cli.feesBps?.[agg] !== undefined) { const num = Number(cli.feesBps[agg]); if (!isNaN(num)) cfg.feeBps = num; else if (!silent) console.warn(`⚠️ Invalid fee BPS for ${agg}: ${cli.feesBps[agg]}`); } }); var displayOutput = (mode, trs, ser) => { const serializationMode = typeof ser === "string" ? ser.toUpperCase() : ser; console.log(mode === DisplayMode.RANK ? serializationMode === SerializationMode.TABLE ? getPerformanceTable(trs) : serialize(trs.map(getPerformance), { mode: serializationMode }) : serialize(compactTrs(mode.includes("BEST") ? [trs[0]] : trs), { mode: serializationMode })); }; // packages/cli/src/log.ts import { getPerformance as getPerformance2, SerializationMode as SerializationMode2, toJSON } from "@btr-supply/swap"; import { Database } from "bun:sqlite"; import { appendFileSync } from "fs"; import { resolve } from "path"; function logPerformance(trs, filePath, mode = SerializationMode2.JSON) { const base = resolve(filePath); const ext = `.${mode.toLowerCase()}`; const file = base.endsWith(ext) ? base : base + ext; const perf = trs.map(getPerformance2); const ts = new Date().toISOString(); try { switch (mode) { case SerializationMode2.JSON: appendFileSync(file, toJSON({ [ts]: { rank: perf, best: perf[0] || {} } }, 0) + ` `); break; case SerializationMode2.SQLITE: const db = new Database(file); db.run("CREATE TABLE IF NOT EXISTS logs (timestamp TEXT PRIMARY KEY, rank TEXT, best TEXT)"); db.run("INSERT OR REPLACE INTO logs VALUES (?, ?, ?)", [ ts, toJSON(perf, 0), toJSON(perf[0] || {}, 0) ]); db.close(); break; default: console.error("Unknown log mode:", mode); } } catch (err) { console.error("Performance log error:", mode, err); } } // packages/cli/src/cli.ts var { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8")); var HELP = ` @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@/ '@@@@/ /@@@/ '@@@@@@@@ @@@@@@@@/ /@@@ @@@@@@/ /@@@@@@@/ /@@@ @@@@@@@ @@@@@@@/ _@@@@@@/ /@@@@@@@/ /. _@@@@@@@@ @@@@@@/ /@@@ '@@@@@/ /@@@@@@@/ /@@ @@@@@@@@@@ @@@@@/ ,@@@@@/ /@@@@@@@/ /@@@, @@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ BTR Swap CLI v${version} - Get quotes from Swap SDK Usage: swap-cli quote [options] or btr-swap quote [options] swap-cli --version or btr-swap --version Options: --input <token> Required. Input token details <chainId:address:symbol:decimals> --input-amount <amount> Required. Amount in wei (e.g., 1000000000000000000 or 1e18) --output <token> Required. Output token details <chainId:address:symbol:decimals> --payer <address> Required. Payer address --receiver <address> Optional. Receiver address (defaults to payer) --max-slippage <bps> Max slippage in bps (default: ${MAX_SLIPPAGE_BPS}) --aggregators <ids> Comma-separated AggIds (default: ${defaultAggregators.join(",")}) --api-keys <json> JSON: multiple API keys, e.g. '{"${AggId2.RANGO}":"key1"}' --referrer-codes <json> JSON: referrer codes, e.g. '{"${AggId2.RANGO}":"ref1"}' --integrator-ids <json> JSON: integrator IDs, e.g. '{"${AggId2.LIFI}":"id1"}' --fees-bps <json> JSON: fee bps, e.g. '{"${AggId2.LIFI}":20}' --display <modes> Comma-separated display modes: ${Object.values(DisplayMode2).join(",")} --serialization <mode> Serialization mode: ${Object.values(SerializationMode3).join(",")} (default: JSON) --env-file <path> Load custom env file --log-file <path> Optional. Path to log file for rank table (relative or absolute) --log-mode <mode> Log mode: JSON,SQLITE (default: JSON) -v, -vv, --verbose Verbose output (-vv for full details) --version Show version and exit -h, --help Show this help message Example: btr-swap quote \\ --input 1:0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:ETH:18 \\ --output 10:0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1:DAI:18 \\ --input-amount 5e17 \\ --payer 0x... \\ --serialization ${SerializationMode3.TABLE} \\ --display ${DisplayMode2.RANK},${DisplayMode2.ALL_COMPACT} \\ --aggregators ${AggId2.RANGO},${AggId2.LIFI} \\ --integrator-ids '{"${AggId2.LIFI}":"integrator-id"}' \\ --api-keys '{"${AggId2.RANGO}":"api-key"}' \\ --serialization ${SerializationMode3.TABLE} `; var runCli = async () => { const args = parseArgs(process.argv.slice(2)); const verbose = typeof args.verbose === "number" ? args.verbose : args.verbose ? 1 : 0; process.env.VERBOSE = verbose.toString(); if (args.version) return console.log(`v${version}`); if (args.help || args._command !== "quote") return console.log(HELP); if (verbose >= 1) console.log("\uD83D\uDE80 Starting BTR Swap CLI..."); try { const envPath = args.envFile; const env = loadEnv(envPath); if (env) { if (verbose >= 1) { const envCount = Object.keys(env).length; const sourcePath = envPath || ".env"; console.log(`✅ Loaded ${envCount} variables from ${sourcePath}`); if (verbose >= 2) console.log(toJSON2(env, 2)); } } else if (envPath && verbose >= 1) { console.log(`⚠️ Environment file not found or empty: ${envPath}`); } const required = ["input", "output", "inputAmount", "payer"]; const missing = required.filter((k) => !args[k]); if (missing.length) handleError(`Missing: ${missing.join(", ")}`); const [inputToken, outputToken] = [args.input, args.output].map((s) => getToken(s)); if (!inputToken || !outputToken) handleError("Invalid tokens"); const amountWei = BigInt(Number(args.inputAmount).toLocaleString("fullwide", { useGrouping: false })); const apiKeys = parseJson("api-keys", args); const referrerCodes = parseJson("referrer-codes", args); const integratorIds = parseJson("integrator-ids", args); const feesBps = parseJson("fees-bps", args); applyConfig({ apiKeys, referrer: referrerCodes, integrators: integratorIds, feesBps }, verbose === 0); if (verbose >= 3) console.log(`[DEBUG] Raw args.serialization: ${args.serialization}`); const params = { input: inputToken, output: outputToken, inputAmountWei: amountWei, payer: args.payer, receiver: args.receiver || args.payer, maxSlippage: args["max-slippage"] ? parseInt(args["max-slippage"]) : MAX_SLIPPAGE_BPS, aggIds: parseEnumArg(args.aggregators, AggId2, defaultAggregators, true), displayModes: parseEnumArg(args.display, DisplayMode2, [DisplayMode2.ALL], true), serializationMode: parseEnumArg(args.serialization, SerializationMode3, SerializationMode3.JSON), logMode: args.logMode?.toUpperCase() === SerializationMode3.SQLITE ? SerializationMode3.SQLITE : SerializationMode3.JSON, executable: args.executable, envFile: envPath, logFile: args.logFile, apiKeys, referrerCodes, integratorIds, feesBps, verbose }; if (verbose >= 1) console.log("⏳ Fetching quotes with params:"); if (verbose >= 2) { console.log(toJSON2(params, 2)); } const trs = await getAllTimedTr(params); if (!trs?.length) handleError("No routes found"); params.displayModes.forEach((m) => displayOutput(m, trs, params.serializationMode)); if (params.logFile) { logPerformance(trs, params.logFile, params.logMode); } } catch (e) { console.error("❌", e instanceof Error ? e.message : String(e)); if (verbose >= 1) console.error("Args:", toJSON2(args, 2)); process.exit(1); } }; runCli();