Skip to content

Swap limits per provider; Spark swaps exit together once the wallet is full - #641

Merged
Jossec101 merged 10 commits into
mainfrom
spark/batched-exits
Oct 16, 2026
Merged

Jossec101 merged 10 commits into
mainfrom
spark/batched-exits

Conversation

@Jossec101

@Jossec101 Jossec101 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
  • Swap limits per provider. Loop, 40swap and Spark each have their own min and max swap size and their own max swaps in flight on the node, instead of one shared set. So Loop can run one swap at a time while Spark runs several.
    • Existing nodes keep their settings: the shared values become Loop's and 40swap's, and Spark's max swaps in flight.
    • Spark's default to SPARK_SWAP_MIN_SATS (50,000) and SPARK_SWAP_MAX_SATS (2,000,000).
    • Auto-liquidity now picks the provider first, among those below their max swaps in flight, then swaps the most that provider takes.
    • The Nodes page shows and edits them per provider, and the grid has a column for each.
  • One exit for many swaps. An exit costs the same whatever its size. So paid swaps now wait in their Spark wallet until it's full (no room for another swap under its max balance), then leave together in one on-chain transaction.
    • One exit per destination wallet, with up to 100 leaves each.
    • The exit fee is shared between the swaps, in proportion to what each brought in.
    • Spark's max swaps in flight counts a swap only while it's being paid.
    • To exit a wallet's swaps earlier, lower its max balance.
  • Fix: payments save their own result. Since Spark swaps: payment timeout, no waiting on create, a Transactions tab and slow exits #640, a Spark swap's background payment used the services of the request that created the swap. When that request ended, saving the payment's result failed, and the monitor recorded it a few minutes later instead. The payment now runs in a scope of its own.
  • One migration for the whole PR: AddPerProviderSwapLimitsAndSparkExitBatches.
  • Tests. Unit tests for each part. The Spark E2E now checks that two swaps share one exit: 7 leaves and one 209-sat fee, the same fee a single-swap exit pays.

…in and max

A node's single Min/Max swap size becomes one pair per provider: the migration renames it to Loop's and copies it to 40swap's. Spark's default to SPARK_SWAP_MIN_SATS (50000, at least 10000) and SPARK_SWAP_MAX_SATS (2000000). Auto-liquidity now picks the provider first, among those whose min fits the excess balance, then swaps the most that provider takes (for Spark, also within its wallet's room).
REBALANCE_MAX_ATTEMPTS, REBALANCE_AMOUNT_BACKOFF_RATIO, REBALANCE_INITIAL_RETRY_DELAY_SECONDS and REBALANCE_RETRY_BACKOFF_MULTIPLIER become SEND_PAYMENT_*, like SEND_PAYMENT_MAX_PARTS: Spark swap payments will retry with them too. The old names are still read as fallbacks.
…nces

A payment that fails for no route, a timeout or too little outbound liquidity is retried up to SEND_PAYMENT_MAX_ATTEMPTS attempts, each SEND_PAYMENT_AMOUNT_BACKOFF_RATIO of the previous one and never below the node's Spark min swap. The SSP only issues fixed-amount invoices, so each retry gets a fresh, smaller one, saved on the swap before the delay so the monitor never sees a failed payment. A persistent Quartz job (SparkSwapPaymentJob) pays it after the backoff delay, and only if the node has no record of it yet. The monitor's deadline follows the current invoice.
A cooperative exit's fee doesn't grow with its leaves, so paid swaps now wait in their Spark wallet and exit in one batch once it is full: the room left under its max balance is below the smallest Spark min swap of the nodes using it. One exit per destination wallet, of at most 100 leaves (open-ssp's limit), to one reserved address saved on every swap first; the on-chain fee is shared in proportion to what each swap brought in. A swap whose leaves can't move leaves the batch. Max swaps in flight counts a Spark swap only until it is paid, and the overdue alert counts from the first exit attempt.
The grid's single Min/Max Swap Amount toggles, which showed nothing, become one pair per provider (Loop, 40swap, Spark), hidden by default and now rendered; Spark's show the setting's value, marked as the default, when the node has none. A column layout saved before keeps its choices: new columns take their default and gone ones are dropped.
@Jossec101
Jossec101 force-pushed the spark/batched-exits branch from 33d450d to 4689c11 Compare October 14, 2026 13:01
@Jossec101 Jossec101 changed the title spark/batched exits Swap sizes per provider; Spark swaps retry smaller and exit together once the wallet is full Oct 14, 2026
AddPerProviderSwapAmounts, AddSparkPaymentRetries and AddSparkExitBatches become AddPerProviderSwapAmountsAndSparkExitBatches: the same schema and the same hand edits (Loop's pair renamed from the shared one and copied to 40swap's, SparkPaymentAttempt defaulting to 1), and the same model snapshot. A local database that applied the three must be recreated.
…at created the swap

The background payment ran on the creating request's services, and their scope ends with the request: the database's context factory with it. The payment went through, but saving its outcome threw ObjectDisposedException, leaving it to MonitorSwapsJob, and a failed first attempt was never retried smaller. Task.Run now opens its own scope and pays with a SparkSwapService of it.
Undo the smaller retries of a failed Spark swap payment: a definitive failure fails the swap again, as before. SparkSwapPaymentJob, the retry audit action and the swaps' SparkPaymentAttempt and SparkInvoiceExpiresAt columns go, and the single migration is regenerated without them. The payment still runs in a scope of its own.
Only rebalances retry now, so REBALANCE_MAX_ATTEMPTS, REBALANCE_AMOUNT_BACKOFF_RATIO, REBALANCE_INITIAL_RETRY_DELAY_SECONDS and REBALANCE_RETRY_BACKOFF_MULTIPLIER keep their names, as on main.
@Jossec101 Jossec101 changed the title Swap sizes per provider; Spark swaps retry smaller and exit together once the wallet is full Swap sizes per provider; Spark swaps exit together once the wallet is full Oct 16, 2026
…eir own

A node's single Max swaps in flight becomes one per provider, so Loop can run one swap at a time while Spark runs several. Auto-liquidity counts each provider's swaps in flight and leaves out a provider at its max, choosing among the others; with every weighted provider at its max it swaps nothing. Spark's creation checks Spark's own. The Nodes page sets each next to the provider's swap sizes, and the grid shows them. The single migration renames the shared value to Loop's and copies it to 40swap's and Spark's.
@Jossec101 Jossec101 changed the title Swap sizes per provider; Spark swaps exit together once the wallet is full Swap limits per provider; Spark swaps exit together once the wallet is full Oct 16, 2026
@Jossec101
Jossec101 marked this pull request as ready for review October 16, 2026 14:45
@Jossec101
Jossec101 requested a review from markettes October 16, 2026 14:58
@Jossec101
Jossec101 enabled auto-merge (squash) October 16, 2026 15:00

@markettes markettes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@Jossec101
Jossec101 disabled auto-merge October 16, 2026 15:13
@Jossec101
Jossec101 merged commit 75b9ec6 into main Oct 16, 2026
6 checks passed
@Jossec101
Jossec101 deleted the spark/batched-exits branch October 16, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants