diff --git a/src/commands/get-balances.ts b/src/commands/get-balances.ts index 1f0ed3c..10a427c 100644 --- a/src/commands/get-balances.ts +++ b/src/commands/get-balances.ts @@ -2,6 +2,25 @@ import { Command } from "commander"; import { BalancesResponse } from "../types.js"; import { getClient, handleError, output } from "../utils.js"; +interface LightningBalanceResponse { + totalSpendable: number; + totalReceivable: number; + nextMaxSpendable: number; + nextMaxReceivable: number; +} + +interface OnchainBalanceResponse { + spendable: number; + total: number; + reserved: number; + pendingBalancesFromChannelClosures: number; +} + +interface APIBalancesResponse { + lightning: LightningBalanceResponse; + onchain: OnchainBalanceResponse; +} + export function registerGetBalancesCommand(program: Command): void { program .command("get-balances") @@ -9,7 +28,31 @@ export function registerGetBalancesCommand(program: Command): void { .action(async () => { await handleError(async () => { const client = getClient(program); - const result = await client.get("/api/balances"); + const apiResult = + await client.get("/api/balances"); + const result: BalancesResponse = { + onchain: { + reservedSat: apiResult.onchain.reserved, + spendableSat: apiResult.onchain.spendable, + totalSat: apiResult.onchain.total, + pendingBalancesFromChannelClosuresSat: + apiResult.onchain.pendingBalancesFromChannelClosures, + }, + lightning: { + nextMaxReceivableSat: Math.floor( + apiResult.lightning.nextMaxReceivable / 1000, + ), + nextMaxSpendableSat: Math.floor( + apiResult.lightning.nextMaxSpendable / 1000, + ), + totalReceivableSat: Math.floor( + apiResult.lightning.totalReceivable / 1000, + ), + totalSpendableSat: Math.floor( + apiResult.lightning.totalSpendable / 1000, + ), + }, + }; output(result); }); }); diff --git a/src/commands/get-channel-suggestions.ts b/src/commands/get-channel-suggestions.ts index 2aa52c1..a115af4 100644 --- a/src/commands/get-channel-suggestions.ts +++ b/src/commands/get-channel-suggestions.ts @@ -14,7 +14,13 @@ export function registerListChannelSuggestionsCommand(program: Command): void { const result = await client.get( "/api/channels/suggestions", ); - output(result); + output( + result.map(({ minimumChannelSize, maximumChannelSize, ...rest }) => ({ + ...rest, + minimumChannelSizeSat: minimumChannelSize, + maximumChannelSizeSat: maximumChannelSize, + })), + ); }); }); } diff --git a/src/commands/list-channels.ts b/src/commands/list-channels.ts index 1ff9d66..534a046 100644 --- a/src/commands/list-channels.ts +++ b/src/commands/list-channels.ts @@ -9,8 +9,29 @@ export function registerListChannelsCommand(program: Command): void { .action(async () => { await handleError(async () => { const client = getClient(program); - const result = await client.get("/api/channels"); - output(result); + const result = await client.get<(Channel & { internalChannel?: unknown })[]>("/api/channels"); + output( + result.map( + ({ + localBalance, + localSpendableBalance, + remoteBalance, + forwardingFeeBaseMsat, + unspendablePunishmentReserve, + counterpartyUnspendablePunishmentReserve, + internalChannel, + ...rest + }) => ({ + ...rest, + localBalanceSat: Math.floor(localBalance / 1000), + localSpendableBalanceSat: Math.floor(localSpendableBalance / 1000), + remoteBalanceSat: Math.floor(remoteBalance / 1000), + forwardingFeeBaseSat: Math.floor(forwardingFeeBaseMsat / 1000), + unspendablePunishmentReserveSat: unspendablePunishmentReserve, + counterpartyUnspendablePunishmentReserveSat: counterpartyUnspendablePunishmentReserve, + }), + ), + ); }); }); } diff --git a/src/commands/list-transactions.ts b/src/commands/list-transactions.ts index 067a09d..e13ac3b 100644 --- a/src/commands/list-transactions.ts +++ b/src/commands/list-transactions.ts @@ -1,6 +1,6 @@ import { Command } from "commander"; import { ListTransactionsResponse } from "../types.js"; -import { getClient, handleError, output } from "../utils.js"; +import { getClient, handleError, mapTransaction, output } from "../utils.js"; export function registerListTransactionsCommand(program: Command): void { program @@ -22,7 +22,10 @@ export function registerListTransactionsCommand(program: Command): void { const result = await client.get( `/api/transactions?${params}`, ); - output(result); + output({ + ...result, + transactions: result.transactions.map(mapTransaction), + }); }); }); } diff --git a/src/commands/lookup-transaction.ts b/src/commands/lookup-transaction.ts index 383393e..73cfcbc 100644 --- a/src/commands/lookup-transaction.ts +++ b/src/commands/lookup-transaction.ts @@ -1,6 +1,6 @@ import { Command } from "commander"; import { Transaction } from "../types.js"; -import { getClient, handleError, output } from "../utils.js"; +import { getClient, handleError, mapTransaction, output } from "../utils.js"; export function registerLookupTransactionCommand(program: Command): void { program @@ -12,7 +12,7 @@ export function registerLookupTransactionCommand(program: Command): void { const result = await client.get( `/api/transactions/${paymentHash}`, ); - output(result); + output(mapTransaction(result)); }); }); } diff --git a/src/commands/make-invoice.ts b/src/commands/make-invoice.ts index 76c35a5..dab0aff 100644 --- a/src/commands/make-invoice.ts +++ b/src/commands/make-invoice.ts @@ -1,6 +1,6 @@ import { Command } from "commander"; import { Transaction } from "../types.js"; -import { getClient, handleError, output } from "../utils.js"; +import { getClient, handleError, mapTransaction, output } from "../utils.js"; export function registerMakeInvoiceCommand(program: Command): void { program @@ -15,7 +15,7 @@ export function registerMakeInvoiceCommand(program: Command): void { amount: opts.amount * 1000, description: opts.description, }); - output(result); + output(mapTransaction(result)); }); }); } diff --git a/src/commands/pay-invoice.ts b/src/commands/pay-invoice.ts index 5fd6fdd..497a344 100644 --- a/src/commands/pay-invoice.ts +++ b/src/commands/pay-invoice.ts @@ -1,6 +1,6 @@ import { Command } from "commander"; import { Transaction } from "../types.js"; -import { getClient, handleError, output } from "../utils.js"; +import { getClient, handleError, mapTransaction, output } from "../utils.js"; export function registerPayInvoiceCommand(program: Command): void { program @@ -18,7 +18,7 @@ export function registerPayInvoiceCommand(program: Command): void { `/api/payments/${invoice}`, Object.keys(body).length > 0 ? body : undefined, ); - output(result); + output(mapTransaction(result)); }); }); } diff --git a/src/commands/request-lsp-order.ts b/src/commands/request-lsp-order.ts index 1c510d3..4d9f9d4 100644 --- a/src/commands/request-lsp-order.ts +++ b/src/commands/request-lsp-order.ts @@ -40,7 +40,14 @@ export function registerRequestLspOrderCommand(program: Command): void { public: opts.public, }, ); - output(result); + const { fee, invoiceAmount, incomingLiquidity, outgoingLiquidity } = result; + output({ + invoice: result.invoice, + feeSat: fee, + invoiceAmountSat: invoiceAmount, + incomingLiquiditySat: incomingLiquidity, + outgoingLiquiditySat: outgoingLiquidity, + }); }); }, ); diff --git a/src/test/e2e/channel-lifecycle.e2e.test.ts b/src/test/e2e/channel-lifecycle.e2e.test.ts index fe7ae13..e71b435 100644 --- a/src/test/e2e/channel-lifecycle.e2e.test.ts +++ b/src/test/e2e/channel-lifecycle.e2e.test.ts @@ -11,6 +11,8 @@ import { waitForChannels, } from "./helpers"; import type { + BalancesResponse, + Channel, ListTransactionsResponse, NodeConnectionInfo, } from "../../types.js"; @@ -139,10 +141,10 @@ test("deposits on-chain funds to hub A", { timeout: 120_000 }, async () => { const balances = await waitForBalances( HUB_A_URL, tokenA, - (b) => b.onchain.spendable > 0, + (b) => b.onchain.spendableSat > 0, 120_000, ); - expect(balances.onchain.spendable).toBeGreaterThan(0); + expect(balances.onchain.spendableSat).toBeGreaterThan(0); }); test("connects hub A as peer to hub B", { timeout: 60_000 }, async () => { @@ -282,10 +284,7 @@ test("opens channel from hub A to hub B", { timeout: 120_000 }, async () => { "list-channels", ]); expect(listChAResult.status).toBe(0); - const listChA = JSON.parse(listChAResult.stdout) as { - remotePubkey: string; - active: boolean; - }[]; + const listChA = JSON.parse(listChAResult.stdout) as Channel[]; expect(Array.isArray(listChA)).toBe(true); const activeChA = listChA.find( (c) => c.remotePubkey === hubBConnInfo.pubkey && c.active, @@ -301,7 +300,7 @@ test("opens channel from hub A to hub B", { timeout: 120_000 }, async () => { "list-channels", ]); expect(listChBResult.status).toBe(0); - const listChB = JSON.parse(listChBResult.stdout) as { active: boolean }[]; + const listChB = JSON.parse(listChBResult.stdout) as Channel[]; expect(Array.isArray(listChB)).toBe(true); expect(listChB.some((c) => c.active)).toBe(true); @@ -346,10 +345,8 @@ test("sends sats from hub A to hub B", { timeout: 120_000 }, async () => { "get-balances", ]); expect(balancesBeforeResult.status).toBe(0); - const balancesBeforeData = JSON.parse(balancesBeforeResult.stdout) as { - lightning: { totalSpendable: number }; - }; - const hubASpendableBefore = balancesBeforeData.lightning.totalSpendable; + const balancesBeforeData = JSON.parse(balancesBeforeResult.stdout) as BalancesResponse; + const hubASpendableBefore = balancesBeforeData.lightning.totalSpendableSat; expect(hubASpendableBefore).toBeGreaterThan(0); // Hub A pays the invoice @@ -372,10 +369,8 @@ test("sends sats from hub A to hub B", { timeout: 120_000 }, async () => { "get-balances", ]); expect(hubABalancesAfterResult.status).toBe(0); - const hubABalancesAfterData = JSON.parse(hubABalancesAfterResult.stdout) as { - lightning: { totalSpendable: number }; - }; - expect(hubABalancesAfterData.lightning.totalSpendable).toBeLessThan( + const hubABalancesAfterData = JSON.parse(hubABalancesAfterResult.stdout) as BalancesResponse; + expect(hubABalancesAfterData.lightning.totalSpendableSat).toBeLessThan( hubASpendableBefore, ); @@ -396,10 +391,8 @@ test("sends sats from hub A to hub B", { timeout: 120_000 }, async () => { tokenB, "get-balances", ]); - const hubBBalancesAfterData = JSON.parse(hubBBalancesAfterResult.stdout) as { - lightning: { totalSpendable: number }; - }; - expect(hubBBalancesAfterData.lightning.totalSpendable).toBeGreaterThan(0); + const hubBBalancesAfterData = JSON.parse(hubBBalancesAfterResult.stdout) as BalancesResponse; + expect(hubBBalancesAfterData.lightning.totalSpendableSat).toBeGreaterThan(0); }); test("sends sats from hub B back to hub A", { timeout: 120_000 }, async () => { @@ -432,9 +425,7 @@ test("sends sats from hub B back to hub A", { timeout: 120_000 }, async () => { expect(hubABalancesBeforeResult.status).toBe(0); const hubABalancesBeforeData = JSON.parse( hubABalancesBeforeResult.stdout, - ) as { - lightning: { totalSpendable: number }; - }; + ) as BalancesResponse; // Record Hub B's balance before payment const hubBBalancesBeforeResult = runCommand([ @@ -447,10 +438,8 @@ test("sends sats from hub B back to hub A", { timeout: 120_000 }, async () => { expect(hubBBalancesBeforeResult.status).toBe(0); const hubBBalancesBeforeData = JSON.parse( hubBBalancesBeforeResult.stdout, - ) as { - lightning: { totalSpendable: number }; - }; - const hubBSpendableBefore = hubBBalancesBeforeData.lightning.totalSpendable; + ) as BalancesResponse; + const hubBSpendableBefore = hubBBalancesBeforeData.lightning.totalSpendableSat; expect(hubBSpendableBefore).toBeGreaterThan(0); // Hub B pays the invoice @@ -473,10 +462,8 @@ test("sends sats from hub B back to hub A", { timeout: 120_000 }, async () => { "get-balances", ]); expect(hubBBalancesAfterResult.status).toBe(0); - const hubBBalancesAfterData = JSON.parse(hubBBalancesAfterResult.stdout) as { - lightning: { totalSpendable: number }; - }; - expect(hubBBalancesAfterData.lightning.totalSpendable).toBeLessThan( + const hubBBalancesAfterData = JSON.parse(hubBBalancesAfterResult.stdout) as BalancesResponse; + expect(hubBBalancesAfterData.lightning.totalSpendableSat).toBeLessThan( hubBSpendableBefore, ); @@ -500,11 +487,9 @@ test("sends sats from hub B back to hub A", { timeout: 120_000 }, async () => { "get-balances", ]); expect(hubABalancesAfterResult.status).toBe(0); - const hubABalancesAfterData = JSON.parse(hubABalancesAfterResult.stdout) as { - lightning: { totalSpendable: number }; - }; - expect(hubABalancesAfterData.lightning.totalSpendable).toBeGreaterThan( - hubABalancesBeforeData.lightning.totalSpendable, + const hubABalancesAfterData = JSON.parse(hubABalancesAfterResult.stdout) as BalancesResponse; + expect(hubABalancesAfterData.lightning.totalSpendableSat).toBeGreaterThan( + hubABalancesBeforeData.lightning.totalSpendableSat, ); // Hub A: list-transactions — should have at least one incoming settled transaction @@ -554,10 +539,7 @@ test( "list-channels", ]); expect(channelsResult.status).toBe(0); - const channels = JSON.parse(channelsResult.stdout) as { - id: string; - remotePubkey: string; - }[]; + const channels = JSON.parse(channelsResult.stdout) as Channel[]; const channel = channels.find( (c) => c.remotePubkey === hubBConnInfo.pubkey, ); @@ -573,10 +555,8 @@ test( "get-balances", ]); expect(balancesBeforeResult.status).toBe(0); - const balancesBeforeData = JSON.parse(balancesBeforeResult.stdout) as { - onchain: { spendable: number }; - }; - const onchainBefore = balancesBeforeData.onchain.spendable; + const balancesBeforeData = JSON.parse(balancesBeforeResult.stdout) as BalancesResponse; + const onchainBefore = balancesBeforeData.onchain.spendableSat; // Close the channel const closeResult = runCommand([ @@ -605,9 +585,9 @@ test( const balancesAfter = await waitForBalances( HUB_A_URL, tokenA, - (b) => b.onchain.spendable > onchainBefore, + (b) => b.onchain.spendableSat > onchainBefore, 120_000, ); - expect(balancesAfter.onchain.spendable).toBeGreaterThan(onchainBefore); + expect(balancesAfter.onchain.spendableSat).toBeGreaterThan(onchainBefore); }, ); diff --git a/src/test/e2e/helpers.ts b/src/test/e2e/helpers.ts index 44a7ec8..62ec663 100644 --- a/src/test/e2e/helpers.ts +++ b/src/test/e2e/helpers.ts @@ -165,6 +165,21 @@ export async function bitcoinRpc( return json.result; } +interface RawAPIBalancesResponse { + lightning: { + totalSpendable: number; + totalReceivable: number; + nextMaxSpendable: number; + nextMaxReceivable: number; + }; + onchain: { + spendable: number; + total: number; + reserved: number; + pendingBalancesFromChannelClosures: number; + }; +} + export async function waitForBalances( url: string, token: string, @@ -178,7 +193,21 @@ export async function waitForBalances( headers: { Authorization: `Bearer ${token}` }, }); if (res.ok) { - const balances = (await res.json()) as BalancesResponse; + const raw = (await res.json()) as RawAPIBalancesResponse; + const balances: BalancesResponse = { + lightning: { + totalSpendableSat: Math.floor(raw.lightning.totalSpendable / 1000), + totalReceivableSat: Math.floor(raw.lightning.totalReceivable / 1000), + nextMaxSpendableSat: Math.floor(raw.lightning.nextMaxSpendable / 1000), + nextMaxReceivableSat: Math.floor(raw.lightning.nextMaxReceivable / 1000), + }, + onchain: { + spendableSat: raw.onchain.spendable, + totalSat: raw.onchain.total, + reservedSat: raw.onchain.reserved, + pendingBalancesFromChannelClosuresSat: raw.onchain.pendingBalancesFromChannelClosures, + }, + }; if (condition(balances)) return balances; } } catch { diff --git a/src/test/e2e/make-offer.e2e.test.ts b/src/test/e2e/make-offer.e2e.test.ts index 9ce5803..bf6705b 100644 --- a/src/test/e2e/make-offer.e2e.test.ts +++ b/src/test/e2e/make-offer.e2e.test.ts @@ -99,7 +99,7 @@ beforeAll(async () => { await waitForBalances( HUB_A_URL, tokenA, - (b) => b.onchain.spendable > 0, + (b) => b.onchain.spendableSat > 0, 120_000, ); diff --git a/src/types.ts b/src/types.ts index eaf592f..103cf03 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,17 +27,17 @@ export interface InfoResponse { } export interface LightningBalanceResponse { - totalSpendable: number; - totalReceivable: number; - nextMaxSpendable: number; - nextMaxReceivable: number; + totalSpendableSat: number; + totalReceivableSat: number; + nextMaxSpendableSat: number; + nextMaxReceivableSat: number; } export interface OnchainBalanceResponse { - spendable: number; - total: number; - reserved: number; - pendingBalancesFromChannelClosures: number; + spendableSat: number; + totalSat: number; + reservedSat: number; + pendingBalancesFromChannelClosuresSat: number; } export interface BalancesResponse { @@ -47,6 +47,7 @@ export interface BalancesResponse { export interface Channel { localBalance: number; + localSpendableBalance: number; remoteBalance: number; remotePubkey: string; fundingTxId: string; diff --git a/src/utils.ts b/src/utils.ts index 9b721e2..ec82d08 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,6 +9,7 @@ import { import { homedir } from "node:os"; import { join } from "node:path"; import { HubClient } from "./client.js"; +import { Transaction } from "./types.js"; function loadAlbyCloudConfig(): { hubName: string } | null { const filePath = join(homedir(), ".hub-cli", "alby-cloud.txt"); @@ -85,6 +86,20 @@ export function getClient(program: Command): HubClient { return new HubClient(url, token, extraHeaders); } +export function mapTransaction( + tx: Transaction, +): Omit & { + amountSat: number; + feesPaidSat: number; +} { + const { amount, feesPaid, ...rest } = tx; + return { + ...rest, + amountSat: Math.floor(amount / 1000), + feesPaidSat: Math.floor(feesPaid / 1000), + }; +} + export function output(data: unknown): void { console.log(JSON.stringify(data, null, 2)); }