* fix(magma): add referrer and verify seller asset-channel support
* refactor(magma): validate buyer node tapd support instead of seller
* feat(assets): gate asset routes on Taproot Assets capability
The nav already hides the Taproot Assets section for non-tapd nodes, but the
routes themselves were unguarded — a typed URL or bookmark to /trading,
/assets, /asset-channels, /asset-transactions or /asset-tools loaded the
feature regardless. Add a RequireTapd route guard that redirects those to
home when the node lacks the capability, and extract a shared useTapdAvailable
hook so the nav and the guard share one signal. /magma stays ungated (it also
serves BTC channels).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: update buy execution path
* chore: attempt to find more routes if one fails
---------
Co-authored-by: Anthony Potdevin <31413433+apotdevin@users.noreply.github.com>
* fix(transactions): format trade memo in invoice and payments cards
- Add formatTradeMemo helper to parse th:trade memo JSON
- Use formatted memo for invoice description and message rendering
- Apply formatTradeMemo to decoded payment request description
* fix(transactions): show computed trade memo marker by mode
- Pass tradeDisplayMode from TransactionsPage into InvoiceCard and PaymentsCard
- Add ComputedMarker tooltip component for computed trade memo display
- Refactor tradeMemo usage to use getTradeMemoText/getTradeMemoDisplay with
* feat(transactions): display raw trade memo details in cards
* chore: changes
---------
Co-authored-by: Anthony Potdevin <potdevin.anthony@gmail.com>
Adds dynamic asset capacity/local/remote/balance-bar columns to the
pending channels view, matching the display already present for open
channels. Also adds asset_name and asset_precision to the
getPendingChannels query so labels and formatted amounts are correct.
Three changes:
- setupTradeCapacity: catch the waitForChannelFromPeer timeout and return
success with channelOpenPending=true instead of throwing; the payment
is in-flight and the channel will open on Magma's schedule
- SidebarTrade: always refetchReadiness after setup (success or error) so
the form reflects the actual order state; show a "channel being set up"
toast when channelOpenPending is true
- fetchPendingOrdersForPeer: drop action_needed filter (role-scoped, excludes
WAITING_FOR_CHANNEL_OPEN for buyers) and filter client-side against all
non-terminal statuses so the pending indicator stays visible while Magma
opens the channel
* feat: add Pay/Cancel action buttons for pending Magma orders
When a pending Magma order (WAITING_FOR_BUYER_PAYMENT) exists for a
selected offer, the Get Quote button is disabled and a prominent yellow
action block is shown with "Pay order" and "Cancel order" buttons so
the user knows what to do next.
Disable `Get Quote` button when there is a pending Magma order
* fix: clarify InsufficientBalanceToAttemptPayment as a routing failure
LND's FAILED_INSUFFICIENT_BALANCE maps to this error but it fires when
no funded route exists, not only when wallet balance is low. Surface a
clearer message so the user knows to check inactive channels or HTLCs.
* fix: link pending order warning to Magma order page
* fix: distinguish unpaid, in-progress and ready Magma orders in offer readiness
Post-payment statuses (WAITING_FOR_CHANNEL_OPEN, VALID_CHANNEL_OPENING,
etc.) were treated identically to unpaid statuses, causing the "unpaid
order blocking trading" warning to appear even after the buyer had paid.
Split the status check into unpaidStatuses (hasPending: true) and
inProgressStatuses (hasPending: false, but still return orderId/status).
Expose pending_order_status in the GraphQL type and query, and update
the sidebar UI to show Pay/Cancel only for unpaid orders.
* feat: enable SALE transaction type in setupTradePartner [AMB-2540]
Remove the placeholder guard that blocked SALE — the full flow
(sats-denominated order size, HODL invoice payment, outbound asset
channel via fundAssetChannel) was already implemented. Replace the
three "throws not-implemented" spec stubs with real assertions.
* fix: skip asset channel open for SALE when one already exists
Add openAssetChannel flag to SetupTradePartnerInput mirroring the
satsAmount opt-out for PURCHASE. The client passes true only when
hasAssetChannel is false; the server skips fundAssetChannel otherwise.
* chore: rename `openAssetChannel` to `openOutboundAssetChannel`
* fix: pass feeRateSatPerVbyte to fundAssetChannel for SALE
tapd requires an explicit on-chain fee rate when opening an asset
channel. Add optional feeRateSatPerVbyte to SetupTradePartnerInput
and default to 1 sat/vbyte on the server when the caller omits it.
* feat: multihop swaps
* fix: fee limit
* fix: circular rebalancing
* chore: simplify and fix post-review issues
- magma: guard allChannels against undefined (??= [])
- magma: restore peer filter on btcOpen and btcPending — both were
accidentally widened to all channels by the multi-hop refactor
- TradingPartners: remove btcChannelPubkeys dead code (filter that used
it was removed; useMemo and stale dep array entry remain)
- trade: move fullHops array from info to debug log to avoid large
object serialisation on every rebalance
- trade: fix misleading 'No bitcoin channels found' error when the real
condition is insufficient outbound liquidity
- test: add debug to mock logger
---------
Co-authored-by: Bufo <bufo24@users.noreply.github.com>
Co-authored-by: Anthony Potdevin <31413433+apotdevin@users.noreply.github.com>
Two related bugs caused the "Connect a Node" setup wizard to error
immediately on startup when using a PostgreSQL backend.
1. Client routing leakage: the Apollo authLink extracts the first URL
segment as the node slug. On the /node-setup route that segment is
literally "node-setup", which was forwarded as x-node-slug on every
GraphQL request from that page.
2. SUBSTR on UUID type: getDbNodeBySlug (and the deleteNode/editNode
helpers in user.service) used SUBSTR(nodes.id, 1, 8) directly. In
SQLite nodes.id is stored as TEXT so SUBSTR works. In PostgreSQL
nodes.id is typed as uuid, and PostgreSQL has no substr(uuid, int,
int) overload — the query raises a type error regardless of the
slug value passed in.
Fix:
- client/config/client.tsx: skip the x-node-slug header when the first
URL segment is a known non-slug public route (login, sso, setup,
node-setup). Extracted into NON_SLUG_PREFIXES set.
- utils/string.ts: add isValidNodeSlug() — returns true only for exactly
8 hex characters (the UUID short-prefix format used by the app).
- accounts.service.ts: guard getDbNodeBySlug with isValidNodeSlug()
before touching the DB; change SUBSTR to SUBSTR(CAST(... AS TEXT), …)
so it is dialect-safe on PostgreSQL.
- user.service.ts: apply the same CAST fix to deleteNode and editNode.
The server-side CAST fix is independently necessary: even after the
client stops leaking "node-setup", all legitimate slug lookups would
still fail on PostgreSQL without it.
Tested on PostgreSQL 18.3 and SQLite. Full test suite passes (149 tests).
Signed-off-by: Kilombino <kilombino@proton.me>
Co-authored-by: Anthony Potdevin <31413433+apotdevin@users.noreply.github.com>
* feat(db): add channel_notes table with Drizzle schema and migrations
Adds a new `channel_notes` table (SQLite + PG) with composite PK
(account_id, channel_id) to persist per-channel notes scoped to each
ThunderHub account. Includes migration files for both database backends.
* feat(api): add ChannelNotesService with getChannelNotes query and setChannelNote mutation
Introduces ChannelNotesService (Drizzle-backed, account-scoped) with
getNotes/upsertNote methods. Exposes two new GraphQL endpoints on
ChannelsResolver: getChannelNotes (query) and setChannelNote (mutation).
* feat(client): add inline channel notes to channels table and details panel
- Adds NoteCell component to ChannelTable with click-to-edit inline input
(Enter/✓ to save, Escape to cancel)
- Fetches all notes on load via getChannelNotes query; updates optimistically
- Adds note textarea + Save button in ChannelDetails modal below fee editor
- Notes are persisted server-side and survive page refreshes
* chore: table changes
* chore: schema cleanup
* chore: cleanup table
* chore: more cleanup
* chore: db ondelete
---------
Co-authored-by: Anthony Potdevin <potdevin.anthony@gmail.com>
The circular rebalance for asset channel sat top-up was failing with
IncorrectCltvExpiry because hopCltvDelta was derived from the canonical
TA channel SCID's gossiped policy. The route actually traverses the
peer's tapd virtual SCID alias (e.g. 16000000x0x1), which has its own
gossiped policy that the canonical-SCID lookup does not return.
Switch to the same source the buy flow uses: enable
is_including_private_channels on createInvoice, decode the resulting
BOLT11, and read cltv_delta / base_fee_mtokens / fee_rate from the TA
channel's route hint. Drop both getChannel calls; the canonical lookup
returned the wrong cltv and the BTC channel max was a defensive
fallback that is no longer needed. Disambiguate the hint by
partner_scid_alias when known so multi-hint invoices pick the right
entry.
Co-authored-by: Bufo <bufo24@users.noreply.github.com>
* feat: circular rebalance for TA channel sat reserve
Before executing a sell trade, check whether the TA channel's local sat
balance is below its reserve requirement. If so, run a circular rebalance:
pay a sats invoice to ourselves routing out through the BTC channel and back
in through all TA channels that are below the reserve requirement, topping up
the sat balance with a 50% buffer above the bare deficit so the asset HTLC
anchor can proceed.
* chore: rename id to accountId
* chore: improve variable and parameter naming in trade resolver
- Rename effectiveReserve → minRequiredBalance (it's the floor balance,
not a modified reserve)
- Rename taChannelPeerAlias → taChannelPartnerScidAlias throughout, and
peerAlias → partnerScidAlias on the intermediate return object, to
match the source field partner_scid_alias and make the SCID nature explicit
- Separate empty-channels path from fetch-error path in findTaChannelsForAsset
so the warning only fires on actual errors
* chore: changes
---------
Co-authored-by: Anthony Potdevin <potdevin.anthony@gmail.com>