@btr-supply/swap-cli
Version:
Command-line interface for the BTR Swap SDK
639 lines (628 loc) • 22.9 kB
JavaScript
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();