Skip to content

swapwallet: Settle Lightning receives over Ark channels - #1139

Open
sputn1ck wants to merge 4 commits into
kon/ark-channel-daemonfrom
kon/modular-ark-channels
Open

swapwallet: Settle Lightning receives over Ark channels#1139
sputn1ck wants to merge 4 commits into
kon/ark-channel-daemonfrom
kon/modular-ark-channels

Conversation

@sputn1ck

@sputn1ck sputn1ck commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

  • bridge completed Lightning receives into hub-funded Ark channel intents
  • surface the resulting channel settlement through the swap client and wallet APIs
  • preserve the funded-vHTLC receive path when a channel receive is not selected
  • keep the backing transaction unpublished on the happy path

This is layer 5 of 5 in the Ark channels stack and depends on #1192. The swapdk-server operator stack consumes this tip.

LND dependency

The series pins sputn1ck/lnd:kon/modular-channel-runtime as a Go module. No nested lnd submodule or lnd PR is required.

Testing

  • go test ./oor ./arkchannel/... ./lnruntime ./waved ./db
  • tagged swap receive package tests
  • make build
  • make lint-changed-local
  • complete daemon E2E coverage lives in the dependent swapdk-server stack

@sputn1ck

Copy link
Copy Markdown
Member Author

/gateway review

@lightninglabs-gateway

Copy link
Copy Markdown

Cannot run /gateway review on a draft PR. Mark it ready for review first.

@sputn1ck
sputn1ck marked this pull request as ready for review August 17, 2026 19:36
Copilot AI lite review requested due to automatic review settings August 17, 2026 19:36

Copilot AI 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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@sputn1ck

Copy link
Copy Markdown
Member Author

/gateway review

@lightninglabs-gateway

lightninglabs-gateway Bot commented Aug 17, 2026

Copy link
Copy Markdown

❌ gateway review failed: could not fetch PR context: {"level":"info","script":"fetch-pr-context.sh","event":"fetch","outcome":"attempt","step":"pr","path":"repos/lightninglabs/wavelength/pulls/1139"} {"level":"info","script":"fetch-pr-context.sh","event":"fetch","outcome":"attempt","step":"diff","path":"repos/lightninglabs/wavelength/pulls/1139"} {"level":"error","script":"fetch-pr-context.sh","event":"fetch_diff","outcome":"failure","message":"fetch diff failed: gh: Sorry, the diff exceeded the maximum number of lines (20000) (HTTP 406) "}

See workflow logs for details.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c0bffeffb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1164 to +1167
if record.Snapshot.Phase != arkchannel.PhaseActive ||
record.Snapshot.Terms.Kind !=
arkchannel.KindReceiveIntent {
return arkchannel.ID{}, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Wait for channel activation before reporting settlement

When the private invoice settles while the receive channel is still PhaseActivating—after lnd activates the link but before the coordinator records ChannelActive—this branch returns a zero channel ID with no error. waitForChannelOrVHTLC consequently persists the swap as successfully completed with SettlementTypeArkChannel and an empty channel ID, losing the correlation needed to inspect or recover the manifested channel. Continue waiting for syncReceiveIntent to establish PhaseActive, or return an error instead of treating this intermediate state as success.

Useful? React with 👍 / 👎.

Comment on lines +1172 to +1181
case err := <-syncResult:
if err != nil && c.cfg.Log != nil {
c.cfg.Log.WarnS(
ctx,
"Receive channel intent stopped",
err,
btclog.Hex("payment_hash", hash[:]),
)
}
syncResult = nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep synchronizing after transient receive-intent errors

If syncReceiveIntent encounters any transient mailbox or store error before activation, this case only logs it and permanently disables the synchronization branch by setting syncResult to nil. The receive then waits for an invoice that cannot settle because the client no longer binds or advances the prepared channel, typically consuming the entire invoice deadline before the swap worker can retry. Retry the durable synchronization operation or propagate the error promptly so the outer worker can resume it.

Useful? React with 👍 / 👎.

@sputn1ck
sputn1ck requested a review from bhandras August 24, 2026 13:34
@sputn1ck
sputn1ck force-pushed the kon/modular-ark-channels branch from 2b9973c to bbded8e Compare August 24, 2026 14:37
@sputn1ck
sputn1ck changed the base branch from main to kon/ark-channel-daemon August 24, 2026 14:37
@sputn1ck
sputn1ck force-pushed the kon/modular-ark-channels branch from bbded8e to bec163d Compare August 24, 2026 15:28
@sputn1ck sputn1ck changed the title waved: add OOR-backed native Ark channels swapwallet: Settle Lightning receives over Ark channels Aug 24, 2026
Carry the reserved SCID and manifested channel identity through swap and
wallet activity responses without reviving the removed fee field.
Race the ordinary funded vHTLC rail against a registered native channel
while persisting enough state to resume either outcome.
Connect daemon-owned invoices and channel settlement to the existing
swap receive lifecycle and keep pending workers restartable.
Expose channel identities in wallet activity without changing the
ordinary wallet receive interface.
@sputn1ck
sputn1ck force-pushed the kon/modular-ark-channels branch from bec163d to 95edaaf Compare August 24, 2026 15:35
@litbot-9000

Copy link
Copy Markdown
Collaborator

Doc advisory

This PR's Go changes left the per-package docs for sdk/swaps,
swapclientserver, rpc/swapclientrpc, rpc/wavewalletrpc, and swaprpc
behind the code: the Ark-channel receive rail and its new exported surface are
undocumented, and swapclientserver/AGENTS.md was never re-mirrored after
swapclientserver/CLAUDE.md was edited in this PR.

Proposed doc changes (advisory only — nothing is committed)
diff --git a/rpc/swapclientrpc/AGENTS.md b/rpc/swapclientrpc/AGENTS.md
index 33aa666c..fe82cfcc 100644
--- a/rpc/swapclientrpc/AGENTS.md
+++ b/rpc/swapclientrpc/AGENTS.md
@@ -14,6 +14,13 @@ credit funding/redemption/listing). Registered only in swapruntime builds.
 - Request/response messages (`QuotePayRequest`, `StartPayRequest`,
   `CreateCreditRequest`, `ListCreditsRequest`, etc.) and enums
   (`SwapState`, `SwapDirection`, `CreditOperationState`, ...).
+- `SwapSettlementType` — how a swap is backed: `LIGHTNING`, `IN_ARK`,
+  `CREDIT`, `MIXED`, and `ARK_CHANNEL` (the receive settled directly into an
+  Ark-backed native Lightning channel).
+- `SwapSummary` — flat durable view of one swap. Beyond the amount/state
+  fields it carries `channel_id` (the Ark channel manifested for a receive,
+  empty when an existing channel was reused) and `reserved_scid` (the future
+  virtual SCID advertised in the invoice).
 
 ## Relationships
 
@@ -25,6 +32,9 @@ credit funding/redemption/listing). Registered only in swapruntime builds.
 
 - Generated from `swap_client.proto` via `make rpc`; do not hand-edit any
   `.pb.go` / `.pb.gw.go` file.
+- `SwapSummary` field 29 is `reserved` under the retired name
+  `channel_backing_fee_sat`. Reserved numbers and names are a wire contract:
+  never reuse them, and reserve rather than delete when a field is dropped.
 
 ## Deep Docs
 
diff --git a/rpc/swapclientrpc/CLAUDE.md b/rpc/swapclientrpc/CLAUDE.md
index 33aa666c..fe82cfcc 100644
--- a/rpc/swapclientrpc/CLAUDE.md
+++ b/rpc/swapclientrpc/CLAUDE.md
@@ -14,6 +14,13 @@ credit funding/redemption/listing). Registered only in swapruntime builds.
 - Request/response messages (`QuotePayRequest`, `StartPayRequest`,
   `CreateCreditRequest`, `ListCreditsRequest`, etc.) and enums
   (`SwapState`, `SwapDirection`, `CreditOperationState`, ...).
+- `SwapSettlementType` — how a swap is backed: `LIGHTNING`, `IN_ARK`,
+  `CREDIT`, `MIXED`, and `ARK_CHANNEL` (the receive settled directly into an
+  Ark-backed native Lightning channel).
+- `SwapSummary` — flat durable view of one swap. Beyond the amount/state
+  fields it carries `channel_id` (the Ark channel manifested for a receive,
+  empty when an existing channel was reused) and `reserved_scid` (the future
+  virtual SCID advertised in the invoice).
 
 ## Relationships
 
@@ -25,6 +32,9 @@ credit funding/redemption/listing). Registered only in swapruntime builds.
 
 - Generated from `swap_client.proto` via `make rpc`; do not hand-edit any
   `.pb.go` / `.pb.gw.go` file.
+- `SwapSummary` field 29 is `reserved` under the retired name
+  `channel_backing_fee_sat`. Reserved numbers and names are a wire contract:
+  never reuse them, and reserve rather than delete when a field is dropped.
 
 ## Deep Docs
 
diff --git a/rpc/wavewalletrpc/AGENTS.md b/rpc/wavewalletrpc/AGENTS.md
index 00d5fbdf..c1425954 100644
--- a/rpc/wavewalletrpc/AGENTS.md
+++ b/rpc/wavewalletrpc/AGENTS.md
@@ -67,6 +67,12 @@ id; unlike `List` it may leak internal correlators, so it is kept out of
 - `ExitMode` — which of the two exits an `Exit` call actually ran
   (`COOPERATIVE` / `UNILATERAL`). `EXIT_MODE_UNSPECIFIED` is what an older
   daemon sends; clients must treat it as "unknown", not as a default.
+- `ActivitySwapTrace` — `InspectActivity`'s per-swap drill-down. Carries the
+  internal correlators `List` deliberately drops, including `channel_id` (the
+  Ark channel manifested by a receive fallback, empty when existing inbound
+  was used) and `reserved_scid` (the virtual SCID advertised in the receive
+  invoice and later installed as the channel's local alias). Field 23 is
+  `reserved` under the retired name `channel_backing_fee_sat`.
 - `FailureDomain` / `Reason*` constants (`failure_reasons.go`) — the
   `google.rpc.ErrorInfo` domain/reason wire contract for failed wallet
   RPCs; existing reason values MUST NOT be renamed.
diff --git a/rpc/wavewalletrpc/CLAUDE.md b/rpc/wavewalletrpc/CLAUDE.md
index 00d5fbdf..c1425954 100644
--- a/rpc/wavewalletrpc/CLAUDE.md
+++ b/rpc/wavewalletrpc/CLAUDE.md
@@ -67,6 +67,12 @@ id; unlike `List` it may leak internal correlators, so it is kept out of
 - `ExitMode` — which of the two exits an `Exit` call actually ran
   (`COOPERATIVE` / `UNILATERAL`). `EXIT_MODE_UNSPECIFIED` is what an older
   daemon sends; clients must treat it as "unknown", not as a default.
+- `ActivitySwapTrace` — `InspectActivity`'s per-swap drill-down. Carries the
+  internal correlators `List` deliberately drops, including `channel_id` (the
+  Ark channel manifested by a receive fallback, empty when existing inbound
+  was used) and `reserved_scid` (the virtual SCID advertised in the receive
+  invoice and later installed as the channel's local alias). Field 23 is
+  `reserved` under the retired name `channel_backing_fee_sat`.
 - `FailureDomain` / `Reason*` constants (`failure_reasons.go`) — the
   `google.rpc.ErrorInfo` domain/reason wire contract for failed wallet
   RPCs; existing reason values MUST NOT be renamed.
diff --git a/sdk/swaps/AGENTS.md b/sdk/swaps/AGENTS.md
index a981539e..ec779ada 100644
--- a/sdk/swaps/AGENTS.md
+++ b/sdk/swaps/AGENTS.md
@@ -18,7 +18,9 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/swap
 - `SwapClient` — top-level entry point. Constructed via `NewSwapClient`
   (no persistence) or `NewSwapClientWithStore` (SQLite-backed). Holds
   an `OutSwapEventReceiver` overridable via
-  `SetOutSwapEventReceiver`.
+  `SetOutSwapEventReceiver`, and an optional
+  `ArkChannelPaymentBridge` installed via
+  `SetArkChannelPaymentBridge`.
 - `PaySession` — Ark-to-Lightning pay FSM:
   `Created → SwapCreated → FundingInitiated → VHTLCFunded →
   WaitingForClaim → Completed` (or `Expired` / `RefundInitiated →
@@ -28,7 +30,10 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/swap
   ClaimInitiated → Completed` (or `Expired` / `NeedsIntervention` /
   `Failed`). `HTLCEventAccepted` is a durable checkpoint persisted
   after the server mailbox event is validated so funding detection
-  resumes without re-driving mailbox delivery.
+  resumes without re-driving mailbox delivery. When an
+  `ArkChannelPaymentBridge` is installed, `InvoiceCreated` can also
+  advance straight to `Completed`: the payment settled inside an
+  Ark-backed native Lightning channel, so there is no vHTLC to claim.
 - `MailboxOutSwapEventReceiver` — mailbox-backed receiver. Pulls
   out-swap HTLC events from a `mailbox/pb` edge keyed by a per-session
   mailbox ID derived from the client identity key and payment hash.
@@ -55,9 +60,18 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/swap
 - `IncomingVHTLCEventReceiver` — interface for receivers that
   handle both Lightning-backed and same-Ark vHTLC events; implemented
   by `MailboxOutSwapEventReceiver`.
-- `SettlementType` — `SettlementTypeLightning`, `SettlementTypeInArk`
-  (returned in `InSwapConfig` identifying how the server bridges
-  payment).
+- `SettlementType` — how a swap is backed: `SettlementTypeLightning`,
+  `SettlementTypeInArk`, `SettlementTypeCredit`, `SettlementTypeMixed`
+  (vHTLC + reserved credit), and `SettlementTypeArkChannel` (the
+  receive settled directly into an active Ark-backed native Lightning
+  channel). Returned in `InSwapConfig` for pay quotes and recorded on
+  `SwapSummary` / `ReceiveResult` for receives.
+- `ArkChannelPaymentBridge` — narrow daemon-owned channel boundary
+  (`PrepareIncomingPayment`, `RegisterIncomingPayment`,
+  `WaitIncomingPayment`) that lets a receive settle over a native
+  Ark channel without the SDK owning invoices or channel state. lnd
+  stays authoritative; the production implementation is
+  `swapclientserver.arkChannelPaymentBridge` over `waved.RPCServer`.
 - `Store` — isolated SQLite persistence. Runs its own migration table
   (`swap_client_schema_migrations`) separate from the main daemon DB.
 - `SwapServerConn` / `GRPCSwapServerConn` — remote swap-server gRPC
@@ -83,7 +97,13 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/swap
 - `PayState` / `ReceiveState` — typed FSM enums with `IsTerminal()` /
   `String()`. `ReceiveState` includes `ReceiveStateHTLCEventAccepted`.
 - `VHTLCConfig`, `InSwapConfig`, `RouteHint` — server-negotiation
-  DTOs. `SwapSummary` — flat list view for persisted sessions.
+  DTOs. `SwapSummary` — flat list view for persisted sessions; it
+  also carries `ChannelID` (the Ark channel manifested for a receive,
+  zero when an existing channel was reused) and `ReservedSCID` (the
+  virtual SCID advertised in the invoice route hint).
+- `ReceiveResult` — returned by `ReceiveSession.Wait` / `Claim`;
+  reports `ChannelID` and `SettlementType` alongside the claimed VTXO
+  outpoint so callers can tell a channel settlement from a vHTLC claim.
 - `RecoveryPolicy` / `DefaultRecoveryPolicy` — governs auto-escalation
   from cooperative vHTLC retry to daemon-owned on-chain recovery
   (arm/escalate/cancel via `DaemonConn`'s VHTLC recovery RPCs).
@@ -110,7 +130,8 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/swap
 ## Relationships
 
 - **Depends on**: `lib/arkscript` (vHTLC policy + claim/refund
-  tapscript paths), `sdk/ark` (type aliases), `swaprpc` (gRPC stubs),
+  tapscript paths), `arkchannel` (`arkchannel.ID` returned by the
+  channel bridge), `sdk/ark` (type aliases), `swaprpc` (gRPC stubs),
   `vtxo` (forfeit sign-request conversion), `mailbox/pb` (edge
   pull/ack), `serverconn` + `serverconn/mailboxpull` (`CompoundMailboxID`,
   `PubKeyMailboxID`, mailbox pull backoff), `db` + `db/migrate` +
@@ -134,6 +155,13 @@ same-Ark vHTLC event: if `outEvents` implements
 otherwise the flow falls back to `WaitOutSwapHtlc` and converts the
 result into an `IncomingVHTLCNotification`.
 
+When an `ArkChannelPaymentBridge` is installed, `prepareInvoice`
+additionally calls `PrepareIncomingPayment` (before the route quote,
+to install the known-preimage native invoice) and
+`RegisterIncomingPayment` (after it, to bind the quoted final-hop
+SCID at the hub). `waitForChannelOrVHTLC` then races
+`WaitIncomingPayment` against the mailbox vHTLC rail.
+
 ## Invariants
 
 - `mutateAndPersist` is the only way to change session state — it
@@ -168,6 +196,20 @@ result into an `IncomingVHTLCNotification`.
   and durably persisted the event. `AckCursor` is `eventSeq + 1`.
 - `ReceiveAuthKey` signing/ECDH is always delegated to the daemon;
   the SDK never holds the raw private key for receive-auth.
+- **Exactly one receive rail may reveal the shared preimage.**
+  `waitForChannelOrVHTLC` cancels only its own child context, so a
+  losing local subscription is torn down without failing the swap. A
+  channel-bridge error while the wait context is still live drops that
+  rail and keeps waiting on the vHTLC rail; a vHTLC error is returned.
+- `SettlementTypeArkChannel` is **receive-only**. Both
+  `validateInSwapPreview` and `validateInSwapQuote` reject it for pay
+  quotes rather than trying to price a channel settlement.
+- Receive rows persist `reserved_scid` (8 bytes big-endian, empty when
+  unset) and `channel_id` (32 bytes, empty when unset).
+  `receiveSummaryFromRow` / `receiveSessionFromRow` reject any other
+  width and a negative `channel_backing_fee_sat` rather than silently
+  truncating. `LatestMigrationVersion` is `2`; bump it with every new
+  file in `migrations/`.
 - Error sentinels (`ErrSwapExpired`, `ErrSwapRefunded`,
   `ErrSwapSummaryNotFound`) are exported; callers use `errors.Is`.
 - The credit ledger is server-authoritative; local state only records
diff --git a/sdk/swaps/CLAUDE.md b/sdk/swaps/CLAUDE.md
index a981539e..ec779ada 100644
--- a/sdk/swaps/CLAUDE.md
+++ b/sdk/swaps/CLAUDE.md
@@ -18,7 +18,9 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/swap
 - `SwapClient` — top-level entry point. Constructed via `NewSwapClient`
   (no persistence) or `NewSwapClientWithStore` (SQLite-backed). Holds
   an `OutSwapEventReceiver` overridable via
-  `SetOutSwapEventReceiver`.
+  `SetOutSwapEventReceiver`, and an optional
+  `ArkChannelPaymentBridge` installed via
+  `SetArkChannelPaymentBridge`.
 - `PaySession` — Ark-to-Lightning pay FSM:
   `Created → SwapCreated → FundingInitiated → VHTLCFunded →
   WaitingForClaim → Completed` (or `Expired` / `RefundInitiated →
@@ -28,7 +30,10 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/swap
   ClaimInitiated → Completed` (or `Expired` / `NeedsIntervention` /
   `Failed`). `HTLCEventAccepted` is a durable checkpoint persisted
   after the server mailbox event is validated so funding detection
-  resumes without re-driving mailbox delivery.
+  resumes without re-driving mailbox delivery. When an
+  `ArkChannelPaymentBridge` is installed, `InvoiceCreated` can also
+  advance straight to `Completed`: the payment settled inside an
+  Ark-backed native Lightning channel, so there is no vHTLC to claim.
 - `MailboxOutSwapEventReceiver` — mailbox-backed receiver. Pulls
   out-swap HTLC events from a `mailbox/pb` edge keyed by a per-session
   mailbox ID derived from the client identity key and payment hash.
@@ -55,9 +60,18 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/swap
 - `IncomingVHTLCEventReceiver` — interface for receivers that
   handle both Lightning-backed and same-Ark vHTLC events; implemented
   by `MailboxOutSwapEventReceiver`.
-- `SettlementType` — `SettlementTypeLightning`, `SettlementTypeInArk`
-  (returned in `InSwapConfig` identifying how the server bridges
-  payment).
+- `SettlementType` — how a swap is backed: `SettlementTypeLightning`,
+  `SettlementTypeInArk`, `SettlementTypeCredit`, `SettlementTypeMixed`
+  (vHTLC + reserved credit), and `SettlementTypeArkChannel` (the
+  receive settled directly into an active Ark-backed native Lightning
+  channel). Returned in `InSwapConfig` for pay quotes and recorded on
+  `SwapSummary` / `ReceiveResult` for receives.
+- `ArkChannelPaymentBridge` — narrow daemon-owned channel boundary
+  (`PrepareIncomingPayment`, `RegisterIncomingPayment`,
+  `WaitIncomingPayment`) that lets a receive settle over a native
+  Ark channel without the SDK owning invoices or channel state. lnd
+  stays authoritative; the production implementation is
+  `swapclientserver.arkChannelPaymentBridge` over `waved.RPCServer`.
 - `Store` — isolated SQLite persistence. Runs its own migration table
   (`swap_client_schema_migrations`) separate from the main daemon DB.
 - `SwapServerConn` / `GRPCSwapServerConn` — remote swap-server gRPC
@@ -83,7 +97,13 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/swap
 - `PayState` / `ReceiveState` — typed FSM enums with `IsTerminal()` /
   `String()`. `ReceiveState` includes `ReceiveStateHTLCEventAccepted`.
 - `VHTLCConfig`, `InSwapConfig`, `RouteHint` — server-negotiation
-  DTOs. `SwapSummary` — flat list view for persisted sessions.
+  DTOs. `SwapSummary` — flat list view for persisted sessions; it
+  also carries `ChannelID` (the Ark channel manifested for a receive,
+  zero when an existing channel was reused) and `ReservedSCID` (the
+  virtual SCID advertised in the invoice route hint).
+- `ReceiveResult` — returned by `ReceiveSession.Wait` / `Claim`;
+  reports `ChannelID` and `SettlementType` alongside the claimed VTXO
+  outpoint so callers can tell a channel settlement from a vHTLC claim.
 - `RecoveryPolicy` / `DefaultRecoveryPolicy` — governs auto-escalation
   from cooperative vHTLC retry to daemon-owned on-chain recovery
   (arm/escalate/cancel via `DaemonConn`'s VHTLC recovery RPCs).
@@ -110,7 +130,8 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/sdk/swap
 ## Relationships
 
 - **Depends on**: `lib/arkscript` (vHTLC policy + claim/refund
-  tapscript paths), `sdk/ark` (type aliases), `swaprpc` (gRPC stubs),
+  tapscript paths), `arkchannel` (`arkchannel.ID` returned by the
+  channel bridge), `sdk/ark` (type aliases), `swaprpc` (gRPC stubs),
   `vtxo` (forfeit sign-request conversion), `mailbox/pb` (edge
   pull/ack), `serverconn` + `serverconn/mailboxpull` (`CompoundMailboxID`,
   `PubKeyMailboxID`, mailbox pull backoff), `db` + `db/migrate` +
@@ -134,6 +155,13 @@ same-Ark vHTLC event: if `outEvents` implements
 otherwise the flow falls back to `WaitOutSwapHtlc` and converts the
 result into an `IncomingVHTLCNotification`.
 
+When an `ArkChannelPaymentBridge` is installed, `prepareInvoice`
+additionally calls `PrepareIncomingPayment` (before the route quote,
+to install the known-preimage native invoice) and
+`RegisterIncomingPayment` (after it, to bind the quoted final-hop
+SCID at the hub). `waitForChannelOrVHTLC` then races
+`WaitIncomingPayment` against the mailbox vHTLC rail.
+
 ## Invariants
 
 - `mutateAndPersist` is the only way to change session state — it
@@ -168,6 +196,20 @@ result into an `IncomingVHTLCNotification`.
   and durably persisted the event. `AckCursor` is `eventSeq + 1`.
 - `ReceiveAuthKey` signing/ECDH is always delegated to the daemon;
   the SDK never holds the raw private key for receive-auth.
+- **Exactly one receive rail may reveal the shared preimage.**
+  `waitForChannelOrVHTLC` cancels only its own child context, so a
+  losing local subscription is torn down without failing the swap. A
+  channel-bridge error while the wait context is still live drops that
+  rail and keeps waiting on the vHTLC rail; a vHTLC error is returned.
+- `SettlementTypeArkChannel` is **receive-only**. Both
+  `validateInSwapPreview` and `validateInSwapQuote` reject it for pay
+  quotes rather than trying to price a channel settlement.
+- Receive rows persist `reserved_scid` (8 bytes big-endian, empty when
+  unset) and `channel_id` (32 bytes, empty when unset).
+  `receiveSummaryFromRow` / `receiveSessionFromRow` reject any other
+  width and a negative `channel_backing_fee_sat` rather than silently
+  truncating. `LatestMigrationVersion` is `2`; bump it with every new
+  file in `migrations/`.
 - Error sentinels (`ErrSwapExpired`, `ErrSwapRefunded`,
   `ErrSwapSummaryNotFound`) are exported; callers use `errors.Is`.
 - The credit ledger is server-authoritative; local state only records
diff --git a/swapclientserver/AGENTS.md b/swapclientserver/AGENTS.md
index 69571f77..19e3f69d 100644
--- a/swapclientserver/AGENTS.md
+++ b/swapclientserver/AGENTS.md
@@ -32,6 +32,11 @@ protocol behavior remain entirely inside `sdk/swaps` and `swapdk-server`.
 - `receiveSessionAdapter` — Adds method accessors over
   `sdk/swaps.ReceiveSession` so both production code and tests share the same
   interface without exposing struct fields.
+- `arkChannelPaymentBridge` — Adapts `sdk/swaps.ArkChannelPaymentBridge` to
+  `waved.RPCServer`'s process-owned native lnd channel controller
+  (`PrepareArkChannelIncomingPayment`, `RegisterArkChannelIncomingPayment`,
+  `WaitArkChannelIncomingPayment`) without an in-process gRPC loop. Installed
+  with `SwapClient.SetArkChannelPaymentBridge` during service construction.
 - `creditServerBridge` / `creditDaemonBridge` — Adapt the subserver and the
   in-process Ark/daemon facade to the `credit` package's `CreditServer` /
   `CreditDaemon` interfaces, so the credit durable-actor subsystem reuses this
@@ -46,6 +51,9 @@ protocol behavior remain entirely inside `sdk/swaps` and `swapdk-server`.
   `MailboxOutSwapEventReceiver` (empty mailbox ID — receiver derives the
   per-swap mailbox from client identity + payment hash) on the
   `SwapClient` so out-swap HTLC events flow over the mailbox transport,
+  installs the `arkChannelPaymentBridge`, publishes the authenticated
+  swapdk-server mailbox edge as `cfg.Swap.ArkChannelMailbox` (cleared again
+  on cleanup) so `waved`'s Ark-channel process can reuse the same transport,
   publishes `cfg.Swap.Backend`/`CreditServer`/`CreditDaemon` bridges,
   registers the gRPC subserver, calls `resumePending` (unless
   `cfg.Swap.SuppressResume`), and returns a cleanup function.
@@ -69,6 +77,8 @@ protocol behavior remain entirely inside `sdk/swaps` and `swapdk-server`.
 
 - **Depends on**: `sdk/swaps` (swap FSM, `SwapClient`, `Store`, session,
   credit types), `sdk/ark` (`WrapDaemonServer`, in-process Ark facade),
+  `arkchannel` (`arkchannel.ID` returned by the channel bridge),
+  `mailbox/pb` (mailbox method names for the wait-for-ready interceptor),
   `waved` (`RPCServer`, `Config`, `SwapConfig`, `SwapSubsystem`), `credit`
   (`CreditServer`/`CreditDaemon` interfaces bridged for the credit actor
   subsystem), `rpc/swapclientrpc` (generated gRPC stubs + proto types).
@@ -88,6 +98,13 @@ protocol behavior remain entirely inside `sdk/swaps` and `swapdk-server`.
 - Worker ownership is process-local and mutex-guarded: at most one goroutine
   drives a given payment hash at any time. `markActive` is the admission gate;
   `markInactive` releases it on goroutine exit.
+- A process-local resume or `Wait` error is not itself terminal. The worker
+  reads the durable summary and retries while the FSM remains pending; it
+  releases ownership only after terminal state, successful completion, missing
+  durable state, or daemon shutdown. `runSwapWorker` is the shared loop for
+  both directions and paces retries with `swapClientService.workerRetryDelay`
+  (`defaultWorkerRetryDelay`, 1s), kept on the service so tests can shorten it
+  without moving retry policy into the swap FSM.
 - The daemon uses `rootCtx` (not the individual RPC contexts) for all
   `ResumePayViaLightning` / `ResumeReceiveViaLightning` calls. A CLI
   disconnect does not cancel an admitted swap.
@@ -128,6 +145,15 @@ protocol behavior remain entirely inside `sdk/swaps` and `swapdk-server`.
   "no filesystem here". It mirrors `waved.ensureDataDir`; keep the two in
   step, since a Node host given an unwritable path should fail there rather
   than at the first database open.
+- `swapServerOperationWaitsForReady` gates gRPC `WaitForReady` on methods that
+  create or advance protocol state. Mailbox `Send` is in that set — a receive
+  ack must survive a swapdk-server reconnect — while mailbox `Pull`, quotes,
+  and credit snapshots stay fail-fast so a poll loop does not block on a dead
+  connection. Add new state-advancing swap or mailbox RPCs to the switch.
+- `SetArkChannelPaymentBridge`, like `SetOutSwapEventReceiver`, must be
+  installed before `resumePending`: the bridge decides whether a receive races
+  the native channel rail against the mailbox vHTLC rail, so a late install
+  would leave revived sessions on the mailbox-only path.
 - `SetOutSwapEventReceiver` must run before any receive worker is started:
   `SwapClient` captures the receiver into the per-swap worker at start time,
   so a late install would leave already-running workers using whatever
diff --git a/swapclientserver/CLAUDE.md b/swapclientserver/CLAUDE.md
index 3277b4db..19e3f69d 100644
--- a/swapclientserver/CLAUDE.md
+++ b/swapclientserver/CLAUDE.md
@@ -32,6 +32,11 @@ protocol behavior remain entirely inside `sdk/swaps` and `swapdk-server`.
 - `receiveSessionAdapter` — Adds method accessors over
   `sdk/swaps.ReceiveSession` so both production code and tests share the same
   interface without exposing struct fields.
+- `arkChannelPaymentBridge` — Adapts `sdk/swaps.ArkChannelPaymentBridge` to
+  `waved.RPCServer`'s process-owned native lnd channel controller
+  (`PrepareArkChannelIncomingPayment`, `RegisterArkChannelIncomingPayment`,
+  `WaitArkChannelIncomingPayment`) without an in-process gRPC loop. Installed
+  with `SwapClient.SetArkChannelPaymentBridge` during service construction.
 - `creditServerBridge` / `creditDaemonBridge` — Adapt the subserver and the
   in-process Ark/daemon facade to the `credit` package's `CreditServer` /
   `CreditDaemon` interfaces, so the credit durable-actor subsystem reuses this
@@ -46,6 +51,9 @@ protocol behavior remain entirely inside `sdk/swaps` and `swapdk-server`.
   `MailboxOutSwapEventReceiver` (empty mailbox ID — receiver derives the
   per-swap mailbox from client identity + payment hash) on the
   `SwapClient` so out-swap HTLC events flow over the mailbox transport,
+  installs the `arkChannelPaymentBridge`, publishes the authenticated
+  swapdk-server mailbox edge as `cfg.Swap.ArkChannelMailbox` (cleared again
+  on cleanup) so `waved`'s Ark-channel process can reuse the same transport,
   publishes `cfg.Swap.Backend`/`CreditServer`/`CreditDaemon` bridges,
   registers the gRPC subserver, calls `resumePending` (unless
   `cfg.Swap.SuppressResume`), and returns a cleanup function.
@@ -69,6 +77,8 @@ protocol behavior remain entirely inside `sdk/swaps` and `swapdk-server`.
 
 - **Depends on**: `sdk/swaps` (swap FSM, `SwapClient`, `Store`, session,
   credit types), `sdk/ark` (`WrapDaemonServer`, in-process Ark facade),
+  `arkchannel` (`arkchannel.ID` returned by the channel bridge),
+  `mailbox/pb` (mailbox method names for the wait-for-ready interceptor),
   `waved` (`RPCServer`, `Config`, `SwapConfig`, `SwapSubsystem`), `credit`
   (`CreditServer`/`CreditDaemon` interfaces bridged for the credit actor
   subsystem), `rpc/swapclientrpc` (generated gRPC stubs + proto types).
@@ -91,7 +101,10 @@ protocol behavior remain entirely inside `sdk/swaps` and `swapdk-server`.
 - A process-local resume or `Wait` error is not itself terminal. The worker
   reads the durable summary and retries while the FSM remains pending; it
   releases ownership only after terminal state, successful completion, missing
-  durable state, or daemon shutdown.
+  durable state, or daemon shutdown. `runSwapWorker` is the shared loop for
+  both directions and paces retries with `swapClientService.workerRetryDelay`
+  (`defaultWorkerRetryDelay`, 1s), kept on the service so tests can shorten it
+  without moving retry policy into the swap FSM.
 - The daemon uses `rootCtx` (not the individual RPC contexts) for all
   `ResumePayViaLightning` / `ResumeReceiveViaLightning` calls. A CLI
   disconnect does not cancel an admitted swap.
@@ -132,6 +145,15 @@ protocol behavior remain entirely inside `sdk/swaps` and `swapdk-server`.
   "no filesystem here". It mirrors `waved.ensureDataDir`; keep the two in
   step, since a Node host given an unwritable path should fail there rather
   than at the first database open.
+- `swapServerOperationWaitsForReady` gates gRPC `WaitForReady` on methods that
+  create or advance protocol state. Mailbox `Send` is in that set — a receive
+  ack must survive a swapdk-server reconnect — while mailbox `Pull`, quotes,
+  and credit snapshots stay fail-fast so a poll loop does not block on a dead
+  connection. Add new state-advancing swap or mailbox RPCs to the switch.
+- `SetArkChannelPaymentBridge`, like `SetOutSwapEventReceiver`, must be
+  installed before `resumePending`: the bridge decides whether a receive races
+  the native channel rail against the mailbox vHTLC rail, so a late install
+  would leave revived sessions on the mailbox-only path.
 - `SetOutSwapEventReceiver` must run before any receive worker is started:
   `SwapClient` captures the receiver into the per-swap worker at start time,
   so a late install would leave already-running workers using whatever
diff --git a/swaprpc/AGENTS.md b/swaprpc/AGENTS.md
index c6c377ca..e8710cd5 100644
--- a/swaprpc/AGENTS.md
+++ b/swaprpc/AGENTS.md
@@ -88,6 +88,10 @@ because they are part of the wire contract, not of either side's logic.
 - `SettlementType.SETTLEMENT_TYPE_UNSPECIFIED` (0) is treated as Lightning
   for backward compatibility with older server responses; do not repurpose
   the zero value.
+- `RequestChannelIdRequest` field 7 and `RequestChannelIdResponse` field 10
+  are `reserved` under the retired name `channel_backing_fee_sat` — receives
+  no longer negotiate a legacy channel fee reserve on the wire. Reserved
+  numbers and names are part of the wire contract; never reuse them.
 - `SwapMailboxEvent` is a proto oneof: read the populated variant, don't
   assume `OutSwapHtlcEvent` is the only case as new event kinds are added.
 
diff --git a/swaprpc/CLAUDE.md b/swaprpc/CLAUDE.md
index c6c377ca..e8710cd5 100644
--- a/swaprpc/CLAUDE.md
+++ b/swaprpc/CLAUDE.md
@@ -88,6 +88,10 @@ because they are part of the wire contract, not of either side's logic.
 - `SettlementType.SETTLEMENT_TYPE_UNSPECIFIED` (0) is treated as Lightning
   for backward compatibility with older server responses; do not repurpose
   the zero value.
+- `RequestChannelIdRequest` field 7 and `RequestChannelIdResponse` field 10
+  are `reserved` under the retired name `channel_backing_fee_sat` — receives
+  no longer negotiate a legacy channel fee reserve on the wire. Reserved
+  numbers and names are part of the wire contract; never reuse them.
 - `SwapMailboxEvent` is a proto oneof: read the populated variant, don't
   assume `OutSwapHtlcEvent` is the only case as new event kinds are added.

What drifted

  • swapclientserver/AGENTS.md — this PR edited CLAUDE.md (the new
    worker-retry invariant) but did not cp it to the sibling AGENTS.md, so
    the pair is out of sync on main's doc-pairing rule.
  • sdk/swaps — new exported surface (ArkChannelPaymentBridge,
    SetArkChannelPaymentBridge, SettlementTypeArkChannel,
    SwapSummary.ChannelID/ReservedSCID, ReceiveResult.ChannelID/
    SettlementType), a new InvoiceCreated → Completed receive-FSM edge, the
    dual-rail waitForChannelOrVHTLC race, and LatestMigrationVersion 1 → 2.
  • swapclientserver — new arkChannelPaymentBridge type, the
    cfg.Swap.ArkChannelMailbox publish/clear in Register, the shared
    runSwapWorker retry loop, and mailbox Send joining the wait-for-ready set.
  • rpc/swapclientrpc / rpc/wavewalletrpc / swaprpc — new
    ARK_CHANNEL settlement value, channel_id / reserved_scid fields, and
    the channel_backing_fee_sat reserved field numbers.

No changes proposed for sdk/swaps/sqlc or swapwallet: their docs describe
the generated row types and the inspection facade at a granularity the new
columns and forwarded trace fields do not invalidate.

How to apply

Save the diff above to a file and git apply it, or run the doc-gardening
skill locally over just these packages:

/doc-gardening sdk/swaps
/doc-gardening swapclientserver
/doc-gardening rpc/swapclientrpc
/doc-gardening rpc/wavewalletrpc
/doc-gardening swaprpc

Either way, finish with make doc-check.


This check is advisory and never fails the build.
Run: https://github.com/lightninglabs/wavelength/pull/1139/checks

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.

3 participants