Skip to content

feat: add swaps and pay-onchain commands - #25

Merged
rolznz merged 4 commits into
masterfrom
feat/swap-out
Jun 25, 2026
Merged

rolznz merged 4 commits into
masterfrom
feat/swap-out

Conversation

@rolznz

@rolznz rolznz commented Jun 25, 2026 •

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added CLI commands for swap-in, swap-out, lookup-swap, and on-chain payments.
    • Expanded the CLI help docs with dedicated Swaps and On-chain sections.
  • Bug Fixes

    • Improved payment handling by requiring either a specific amount or “send all” for on-chain sends.
  • Documentation

    • Simplified the Balances section and updated command examples and output guidance.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rolznz, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 48 minutes and 1 second. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8baf3ad-ff48-46bb-8cbf-6f4858e89cdd

📥 Commits

Reviewing files that changed from the base of the PR and between 7f19ad3 and 9fcbb01.

📒 Files selected for processing (2)
  • src/commands/pay-onchain.ts
  • src/commands/swap-out.ts
📝 Walkthrough

Walkthrough

The CLI adds swap-in, swap-out, lookup-swap, and pay-onchain commands, new swap/on-chain API types, and updated README usage examples. The balances section is shortened, and the command reference tables are removed.

Changes

Swap and on-chain CLI

Layer / File(s) Summary
Swap contracts and initiation commands
src/types.ts, src/commands/swap-in.ts, src/commands/swap-out.ts
Adds swap response/state types and command handlers that create swaps, fetch the created swap, and print the full swap payload.
Lookup and on-chain payment commands
src/commands/lookup-swap.ts, src/commands/pay-onchain.ts
Adds lookup-swap and pay-onchain command handlers for fetching swap status and redeeming on-chain funds with option validation.
CLI registration
src/index.ts
Imports the new command modules and registers the new subcommands on the root program.
README usage updates
README.md
Renames the balances section, adds swap/on-chain usage examples, and removes the command reference tables.

Sequence Diagram(s)

Swap commands

sequenceDiagram
  participant registerSwapInCommand
  participant registerSwapOutCommand
  participant client
  participant HubAPI
  participant output
  registerSwapInCommand->>client: POST /api/swaps/in with swapAmountSat
  client->>HubAPI: create swap
  HubAPI-->>client: swapId
  registerSwapInCommand->>client: GET /api/swaps/{swapId}
  client->>HubAPI: fetch Swap
  HubAPI-->>client: Swap
  registerSwapInCommand->>output: print Swap
  registerSwapOutCommand->>client: POST /api/swaps/out with swapAmountSat and destination
  client->>HubAPI: create swap
  HubAPI-->>client: swapId
  registerSwapOutCommand->>client: GET /api/swaps/{swapId}
  client->>HubAPI: fetch Swap
  HubAPI-->>client: Swap
  registerSwapOutCommand->>output: print Swap
Loading

On-chain payment

sequenceDiagram
  participant registerPayOnchainCommand
  participant client
  participant HubAPI
  participant output
  registerPayOnchainCommand->>registerPayOnchainCommand: validate amount/all options
  registerPayOnchainCommand->>client: POST /api/wallet/redeem-onchain-funds
  client->>HubAPI: redeem on-chain funds
  HubAPI-->>client: txId
  registerPayOnchainCommand->>output: print txId
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I hopped through swaps both bright and new,
On-chain carrots paid the bill right through.
A lookup here, a txId वहां?—no, just there,
And JSON crumbs in the CLI air.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding swap-related and pay-onchain CLI commands.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/swap-out

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

324-326: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the output contract wording.

The README says errors are written to stderr with a message field, but the CLI helper emits JSON via console.log and wraps failures as { error: ... }. That mismatch will confuse anyone wiring automation against the docs.

♻️ Proposed fix
-All commands output JSON to stdout. Errors are written to stderr as JSON with a `message` field.
+All commands output JSON to stdout. Errors are written to stdout as JSON with an `error` field.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 324 - 326, The README output contract is inconsistent
with the CLI behavior: it says errors go to stderr with a message field, but the
CLI helper currently emits JSON through console.log and wraps failures as an
error field. Update the wording in the Output section to match the actual
behavior of the CLI helper and its failure shape, referencing the CLI helper’s
error serialization and output path so the docs align with what users will
receive.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/pay-onchain.ts`:
- Around line 36-49: In pay-onchain command handling, the validation in the main
command flow only checks presence and mutual exclusivity of opts.amount and
opts.all, so invalid parseInt results can still reach the API. Add explicit
guards in the pay-onchain command before getClient(program) and the
/api/wallet/redeem-onchain-funds call to reject opts.amount and opts.feeRate
when they are provided but not positive integers, alongside the existing
opts.all checks.

In `@src/commands/swap-in.ts`:
- Around line 11-21: The `swap-in` command’s `--amount` parsing in `handleError`
can produce invalid request payloads because `parseInt` allows `NaN`, zero, and
negative values. Update the `requiredOption`/`.action` flow in `swap-in.ts` to
validate `opts.amount` before calling `client.post` so only finite integers
greater than zero are accepted, and reject invalid input with a clear error
before constructing the `/api/swaps/in` request.

In `@src/commands/swap-out.ts`:
- Around line 23-26: The swap-out request payload is always sending destination
as an empty string when the option is omitted, which breaks optional-field
handling. Update the request construction in swap-out.ts around the post call so
the payload only includes destination when opts.destination is actually
provided, using the swap-out command logic and the client.post<SwapResponse>
call as the place to adjust.
- Around line 11-25: Validate the `--amount` option in the `action` handler for
`swap-out` before calling `client.post` in the `handleError` block: `parseInt`
can still yield `NaN`, zero, or negative values, so add a guard on `opts.amount`
(e.g. check it is a positive integer) and throw the requested error message when
invalid. Use the existing `action(async (opts) => ...)` flow and the
`opts.amount` field to locate the fix.

---

Outside diff comments:
In `@README.md`:
- Around line 324-326: The README output contract is inconsistent with the CLI
behavior: it says errors go to stderr with a message field, but the CLI helper
currently emits JSON through console.log and wraps failures as an error field.
Update the wording in the Output section to match the actual behavior of the CLI
helper and its failure shape, referencing the CLI helper’s error serialization
and output path so the docs align with what users will receive.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7592dd89-3eb8-4466-a35b-37ae610f8521

📥 Commits

Reviewing files that changed from the base of the PR and between bcc122b and 7f19ad3.

📒 Files selected for processing (7)
  • README.md
  • src/commands/lookup-swap.ts
  • src/commands/pay-onchain.ts
  • src/commands/swap-in.ts
  • src/commands/swap-out.ts
  • src/index.ts
  • src/types.ts

Comment on lines +36 to +49
if (!opts.all && opts.amount === undefined) {
throw new Error("specify --amount <sats> or --all");
}
if (opts.all && opts.amount !== undefined) {
throw new Error("--amount and --all are mutually exclusive");
}
const client = getClient(program);
const result = await client.post<RedeemOnchainFundsResponse>(
"/api/wallet/redeem-onchain-funds",
{
toAddress: address,
amountSat: opts.amount,
feeRate: opts.feeRate,
sendAll: opts.all,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/commands/pay-onchain.ts | head -70

Repository: getAlby/hub-cli

Length of output: 2500


Add numeric validation for --amount and --fee-rate.

The options are parsed via parseInt, which returns NaN for invalid inputs. Currently, the code checks only for the presence of --amount and mutual exclusivity with --all, but allows NaN or non-positive values to be sent to the API. Add guards to ensure both are positive integers if specified.

Suggested fix
           if (!opts.all && opts.amount === undefined) {
             throw new Error("specify --amount <sats> or --all");
           }
           if (opts.all && opts.amount !== undefined) {
             throw new Error("--amount and --all are mutually exclusive");
           }
+          if (
+            opts.amount !== undefined &&
+            (Number.isNaN(opts.amount) || opts.amount <= 0)
+          ) {
+            throw new Error("--amount must be a positive integer (sats)");
+          }
+          if (
+            opts.feeRate !== undefined &&
+            (Number.isNaN(opts.feeRate) || opts.feeRate <= 0)
+          ) {
+            throw new Error("--fee-rate must be a positive integer");
+          }
           const client = getClient(program);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!opts.all && opts.amount === undefined) {
throw new Error("specify --amount <sats> or --all");
}
if (opts.all && opts.amount !== undefined) {
throw new Error("--amount and --all are mutually exclusive");
}
const client = getClient(program);
const result = await client.post<RedeemOnchainFundsResponse>(
"/api/wallet/redeem-onchain-funds",
{
toAddress: address,
amountSat: opts.amount,
feeRate: opts.feeRate,
sendAll: opts.all,
if (!opts.all && opts.amount === undefined) {
throw new Error("specify --amount <sats> or --all");
}
if (opts.all && opts.amount !== undefined) {
throw new Error("--amount and --all are mutually exclusive");
}
if (
opts.amount !== undefined &&
(Number.isNaN(opts.amount) || opts.amount <= 0)
) {
throw new Error("--amount must be a positive integer (sats)");
}
if (
opts.feeRate !== undefined &&
(Number.isNaN(opts.feeRate) || opts.feeRate <= 0)
) {
throw new Error("--fee-rate must be a positive integer");
}
const client = getClient(program);
const result = await client.post<RedeemOnchainFundsResponse>(
"/api/wallet/redeem-onchain-funds",
{
toAddress: address,
amountSat: opts.amount,
feeRate: opts.feeRate,
sendAll: opts.all,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/pay-onchain.ts` around lines 36 - 49, In pay-onchain command
handling, the validation in the main command flow only checks presence and
mutual exclusivity of opts.amount and opts.all, so invalid parseInt results can
still reach the API. Add explicit guards in the pay-onchain command before
getClient(program) and the /api/wallet/redeem-onchain-funds call to reject
opts.amount and opts.feeRate when they are provided but not positive integers,
alongside the existing opts.all checks.

Comment thread src/commands/swap-in.ts
Comment on lines +11 to +21
.requiredOption(
"--amount <sats>",
"Amount to receive on lightning, in satoshis",
parseInt,
)
.action(async (opts: { amount: number }) => {
await handleError(async () => {
const client = getClient(program);
const initiated = await client.post<SwapResponse>("/api/swaps/in", {
swapAmountSat: opts.amount,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

Validate --amount to prevent invalid payloads.

parseInt accepts malformed strings (e.g., "abc" results in NaN) which JSON.stringify converts to null, sending an invalid payload to the API. It also allows negative numbers and zero. Add validation to ensure the value is a finite integer and strictly greater than zero before construction of the request.

Suggested fix
     .action(async (opts: { amount: number }) => {
       await handleError(async () => {
+        if (!Number.isFinite(opts.amount) || !Number.isInteger(opts.amount) || opts.amount <= 0) {
+          throw new Error("--amount must be a positive integer (sats)");
+        }
         const client = getClient(program);
         const initiated = await client.post<SwapResponse>("/api/swaps/in", {
           swapAmountSat: opts.amount,
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.requiredOption(
"--amount <sats>",
"Amount to receive on lightning, in satoshis",
parseInt,
)
.action(async (opts: { amount: number }) => {
await handleError(async () => {
const client = getClient(program);
const initiated = await client.post<SwapResponse>("/api/swaps/in", {
swapAmountSat: opts.amount,
});
.requiredOption(
"--amount <sats>",
"Amount to receive on lightning, in satoshis",
parseInt,
)
.action(async (opts: { amount: number }) => {
await handleError(async () => {
if (!Number.isFinite(opts.amount) || !Number.isInteger(opts.amount) || opts.amount <= 0) {
throw new Error("--amount must be a positive integer (sats)");
}
const client = getClient(program);
const initiated = await client.post<SwapResponse>("/api/swaps/in", {
swapAmountSat: opts.amount,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/swap-in.ts` around lines 11 - 21, The `swap-in` command’s
`--amount` parsing in `handleError` can produce invalid request payloads because
`parseInt` allows `NaN`, zero, and negative values. Update the
`requiredOption`/`.action` flow in `swap-in.ts` to validate `opts.amount` before
calling `client.post` so only finite integers greater than zero are accepted,
and reject invalid input with a clear error before constructing the
`/api/swaps/in` request.

Comment thread src/commands/swap-out.ts Outdated
Comment on lines +11 to +25
.requiredOption(
"--amount <sats>",
"Amount to receive on-chain, in satoshis",
parseInt,
)
.option(
"--destination <address>",
"External on-chain address to receive the funds. Omit to swap into the hub's own on-chain wallet.",
)
.action(async (opts: { amount: number; destination?: string }) => {
await handleError(async () => {
const client = getClient(program);
const initiated = await client.post<SwapResponse>("/api/swaps/out", {
swapAmountSat: opts.amount,
destination: opts.destination ?? "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

Validate --amount as a positive integer.

parseInt allows invalid inputs like NaN, 0, or negative numbers, which create malformed payloads.

Add a check before the API call:
if (isNaN(opts.amount) || opts.amount <= 0) throw new Error("Amount must be a positive integer");

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/swap-out.ts` around lines 11 - 25, Validate the `--amount`
option in the `action` handler for `swap-out` before calling `client.post` in
the `handleError` block: `parseInt` can still yield `NaN`, zero, or negative
values, so add a guard on `opts.amount` (e.g. check it is a positive integer)
and throw the requested error message when invalid. Use the existing
`action(async (opts) => ...)` flow and the `opts.amount` field to locate the
fix.

Comment thread src/commands/swap-out.ts
@rolznz
rolznz merged commit 0fd0405 into master Jun 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant