Skip to content
146 changes: 131 additions & 15 deletions bitflow/bitflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type HodlmmActiveBinToleranceInput,
type HodlmmRelativeLiquidityBinInput,
type HodlmmRelativeWithdrawalInput,
type HodlmmWithdrawalInput,
type UnifiedBitflowRouteQuote,
} from "../src/lib/services/bitflow.service.js";
import { resolveFee } from "../src/lib/utils/fee.js";
Expand Down Expand Up @@ -58,7 +59,7 @@ function normalizeRelativeLiquidityBins(rawBins: unknown): HodlmmRelativeLiquidi
});
}

function normalizeRelativeWithdrawalPositions(rawPositions: unknown): HodlmmRelativeWithdrawalInput[] {
function normalizeWithdrawalPositions(rawPositions: unknown): HodlmmRelativeWithdrawalInput[] {
if (!Array.isArray(rawPositions) || rawPositions.length === 0) {
throw new Error("--positions must be a non-empty JSON array");
}
Expand All @@ -74,7 +75,7 @@ function normalizeRelativeWithdrawalPositions(rawPositions: unknown): HodlmmRela
const minXAmount = value.minXAmount ?? value.min_x_amount ?? 0;
const minYAmount = value.minYAmount ?? value.min_y_amount ?? 0;

if (typeof activeBinOffset !== "number") {
if (activeBinOffset === undefined || typeof activeBinOffset !== "number") {
throw new Error(`positions[${index}].activeBinOffset must be a number`);
}

Expand All @@ -91,6 +92,39 @@ function normalizeRelativeWithdrawalPositions(rawPositions: unknown): HodlmmRela
});
}

function normalizeAbsoluteWithdrawalPositions(rawPositions: unknown): HodlmmWithdrawalInput[] {
if (!Array.isArray(rawPositions) || rawPositions.length === 0) {
throw new Error("--positions must be a non-empty JSON array");
}

return rawPositions.map((position, index) => {
if (!position || typeof position !== "object") {
throw new Error(`positions[${index}] must be an object`);
}

const value = position as Record<string, unknown>;
const binId = value.binId ?? value.bin_id;
const amount = value.amount;
const minXAmount = value.minXAmount ?? value.min_x_amount ?? 0;
const minYAmount = value.minYAmount ?? value.min_y_amount ?? 0;

if (binId === undefined || typeof binId !== "number") {
throw new Error(`positions[${index}].binId must be a number`);
}

if (amount === undefined || amount === null) {
throw new Error(`positions[${index}].amount is required`);
}

return {
binId,
amount: String(amount),
minXAmount: String(minXAmount),
minYAmount: String(minYAmount),
};
});
}

function normalizeActiveBinTolerance(raw: unknown): HodlmmActiveBinToleranceInput {
if (!raw || typeof raw !== "object") {
throw new Error("--active-bin-tolerance must be a JSON object");
Expand Down Expand Up @@ -314,7 +348,7 @@ program
.name("bitflow")
.description(
"Bitflow DEX: token swaps, market data, routing, and Keeper automation on Stacks. Mainnet-only. " +
"No API key required — uses public endpoints (500 req/min)."
"No API key required — uses public endpoints (500 req/min)."
)
.version("0.1.0");

Expand All @@ -326,7 +360,7 @@ program
.command("get-ticker")
.description(
"Get market ticker data from Bitflow DEX. Returns price, volume, and liquidity data for all trading pairs. " +
"No API key required. Mainnet-only."
"No API key required. Mainnet-only."
)
.option(
"--base-currency <contractId>",
Expand Down Expand Up @@ -405,7 +439,7 @@ program
.command("get-tokens")
.description(
"Get all available tokens for swapping on Bitflow. " +
"No API key required — uses public endpoints (500 req/min). Mainnet-only."
"No API key required — uses public endpoints (500 req/min). Mainnet-only."
)
.action(async () => {
try {
Expand Down Expand Up @@ -593,7 +627,7 @@ program
.command("get-swap-targets")
.description(
"Get possible swap target tokens for a given input token on Bitflow. " +
"Returns all tokens that can be received when swapping from the specified token. Mainnet-only."
"Returns all tokens that can be received when swapping from the specified token. Mainnet-only."
)
.requiredOption(
"--token-id <contractId>",
Expand Down Expand Up @@ -688,7 +722,7 @@ program
.command("get-routes")
.description(
"Get all possible swap routes between two tokens on Bitflow. " +
"Includes multi-hop routes through intermediate tokens. Mainnet-only."
"Includes multi-hop routes through intermediate tokens. Mainnet-only."
)
.requiredOption(
"--token-x <tokenId>",
Expand Down Expand Up @@ -739,7 +773,7 @@ program
.command("swap")
.description(
"Execute a token swap on Bitflow DEX. Automatically finds the best route across all Bitflow pools. " +
"Requires an unlocked wallet with sufficient token balance. Mainnet-only."
"Requires an unlocked wallet with sufficient token balance. Mainnet-only."
)
.requiredOption(
"--token-x <tokenId>",
Expand Down Expand Up @@ -916,8 +950,8 @@ program
);
const activeBinTolerance = opts.activeBinTolerance
? normalizeActiveBinTolerance(
parseJsonOption<unknown>(opts.activeBinTolerance, "--active-bin-tolerance")
)
parseJsonOption<unknown>(opts.activeBinTolerance, "--active-bin-tolerance")
)
: undefined;
const resolvedFee = await resolveFee(opts.fee, NETWORK, "contract_call");
const result = await bitflowService.addHodlmmLiquiditySimple({
Expand Down Expand Up @@ -1001,7 +1035,7 @@ program

const bitflowService = getBitflowService(NETWORK);
const account = await getWriteAccount(opts.walletPassword);
const positions = normalizeRelativeWithdrawalPositions(
const positions = normalizeWithdrawalPositions(
parseJsonOption<unknown>(opts.positions, "--positions")
);
const resolvedFee = await resolveFee(opts.fee, NETWORK, "contract_call");
Expand Down Expand Up @@ -1030,6 +1064,88 @@ program
}
);

// ---------------------------------------------------------------------------
// withdraw-liquidity
// ---------------------------------------------------------------------------

program
.command("withdraw-liquidity")
.description(
"Withdraw HODLMM liquidity using absolute bin IDs. Requires an unlocked wallet. Mainnet-only."
)
.requiredOption("--pool-id <poolId>", "HODLMM pool ID (e.g. dlmm_6)")
.requiredOption(
"--positions <json>",
"JSON array of positions to withdraw, e.g. '[{\"binId\":258,\"amount\":\"392854\",\"minXAmount\":\"0\",\"minYAmount\":\"0\"}]'"
)
.option(
"--pool-contract <contractId>",
"Override pool contract identifier if needed"
)
.option(
"--x-token-contract <contractId>",
"Override token X contract identifier if needed"
)
.option(
"--y-token-contract <contractId>",
"Override token Y contract identifier if needed"
)
.option("--allow-fallback", "Enable on-chain fallback when reading pool/bin metadata")
.option(
"--fee <value>",
"Optional STX fee: 'low' | 'medium' | 'high' preset or micro-STX amount"
)
.option(
"--wallet-password <password>",
"Optional wallet password to unlock the active managed wallet for this command"
)
.action(
async (opts: {
poolId: string;
positions: string;
poolContract?: string;
xTokenContract?: string;
yTokenContract?: string;
allowFallback?: boolean;
fee?: string;
walletPassword?: string;
}) => {
try {
if (NETWORK !== "mainnet") {
printJson({ error: "Bitflow is only available on mainnet", network: NETWORK });
return;
}

const bitflowService = getBitflowService(NETWORK);
const account = await getWriteAccount(opts.walletPassword);
const positions = normalizeAbsoluteWithdrawalPositions(
parseJsonOption<unknown>(opts.positions, "--positions")
);
const resolvedFee = await resolveFee(opts.fee, NETWORK, "contract_call");
const result = await bitflowService.withdrawHodlmmLiquidity({
account,
poolId: opts.poolId,
positions,
allowFallback: opts.allowFallback,
fee: resolvedFee,
poolContract: opts.poolContract,
xTokenContract: opts.xTokenContract,
yTokenContract: opts.yTokenContract,
});

printJson({
success: true,
network: NETWORK,
txid: result.txid,
poolId: opts.poolId,
explorerUrl: getExplorerTxUrl(result.txid, NETWORK),
});
} catch (error) {
handleError(error);
}
}
);

// ---------------------------------------------------------------------------
// get-keeper-contract
// ---------------------------------------------------------------------------
Expand All @@ -1038,7 +1154,7 @@ program
.command("get-keeper-contract")
.description(
"Get or create a Bitflow Keeper contract for automated swaps. " +
"Keeper contracts enable scheduled/automated token swaps. Mainnet-only."
"Keeper contracts enable scheduled/automated token swaps. Mainnet-only."
)
.option(
"--address <stacksAddress>",
Expand Down Expand Up @@ -1136,9 +1252,9 @@ program
actionAmount: opts.actionAmount,
minReceived: opts.minReceivedAmount
? {
amount: opts.minReceivedAmount,
autoAdjust: opts.autoAdjust ?? true,
}
amount: opts.minReceivedAmount,
autoAdjust: opts.autoAdjust ?? true,
}
: undefined,
});

Expand Down
Loading
Loading