Skip to content

feat(lnurl): LNURL send, receive, token rails and a Lightning address page - #1084

Merged
Kukks merged 78 commits into
masterfrom
split/p12-settings-lnaddress
Oct 28, 2026
Merged

Kukks merged 78 commits into
masterfrom
split/p12-settings-lnaddress

Conversation

@Kukks

@Kukks Kukks commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

LNURL in the wallet, on @arkade-os/lnurl-client 0.8.0, in one PR:

  • record and display LNURL payments;
  • receive through the configured LNURL server;
  • token receive rails;
  • Settings → Lightning address.

This folds in #1073, #1078, #1081 and #1083, which were stacked under this PR. Their commits are unchanged. #1109 is merged in too: claim codes that carry their own name, and the primary-address picker. The receive redesign (#1086) stays a separate PR stacked on this one, and draft #1091 sits on #1086.

Dependencies

  • Pin @arkade-os/lnurl-client to the published 0.9.1 and @web3icons/react to 4.1.24.
    • 0.8.0 dropped HD receive, which the wallet never used.
    • 0.9.x adds claim codes that carry their own name and owned({ handle }), which the wallet uses.
    • It also adds registered payment methods and receive amounts, which the wallet does not use; a facade given no methods never touches them.
  • Includes merged fix(bip21): read a Lightning address back from lightning= #1079: Bitcoin links round-trip Lightning addresses and escape query delimiters.
  • Merged with master 03c90955. It pins SDK 0.5.0-rc.28 and swap 0.1.0-rc.31 (Upgrade sdk@0.5.0-rc.28 and swap@0.1.0-rc.31 #1108), moves tests to vitest 5 (Security: vitest update + shadcn removal #1103), and adds an i18n check that fails on any key not used as a literal t('...') (I18n/cleanup #1107). Those pins are kept. The claim error messages are now literal t() calls, so the check sees them in use.

Send and payment history

  • Record and display LNURL sends and receives with their address, rail and receiver confirmation.
  • Includes the send routing and signing work merged from feat(send): pay LNURL targets through the router, and track the receiver's confirmation #1080. Quotes are bound to the selected recipient and amount, stale form results are discarded, funding/settlement records are kept, and verification resumes after a restart.
  • Use the package's repository/reset, IndexedDB and localStorage stores, confirmations and send tracker (subscribe, all). Activity rows come from lnurlActivity, the send form's ranges from lnurlPayableRanges, and LNURL routing from firstQuote / isLnurlRail.
  • The wallet keeps its singleton adapters, existing storage keys and schema, activity attribution and UI.
  • Reset closes and drains payment and watermark writes. It clears cursors before records, and stops late send subscriptions before clearing localStorage. A failed preimage purge stops a destructive reset.
  • Open receipts update as soon as a confirmation is written in the same tab.
  • Refusals on the Details screen show in the wallet's language.
  • Driving-tab LNURL routing skips the unrelated onchain claim-fee request.
  • If this tab's swap client failed to start, an LNURL send still routes. Its router is built without the client, as on a tab that is not driving (CodeRabbit).
  • The Ark-method address request keeps a query the LNURL already carries, adding method=ark beside it instead of a second ? (CodeRabbit).
  • A reset whose LNURL purge fails leaves the wallet intact and still updating balance and history. Only the LNURL writers stop before the purge; the listeners stop after it succeeds (CodeRabbit).
  • Max to an LNURL takes the rail's fee out of the amount: it re-quotes for the balance less the quoted fee, and shows the amount that will arrive. Before, a rail with a fee (the Lightning swap) quoted a total above the balance, and the sign screen refused with "Insufficient funds". A typed amount is left as typed.

Receive

  • Receive through the configured LNURL server: local discovery, address naming, Lightning invoices, and optional Arkade/Bitcoin rails.
  • Fall back to the swap rail when the server fails, or when onboarding offers no self-claim action. Four unclaimable onboarding variants negotiate a swap and include its invoice in the copied QR.
  • Use the package's receiver bootstrap, rail resolver, sessionLnurl() (the wallet LNURL shown before registration lands) and addresses(). Claim errors map through the typed LnurlAddressErrorCode.
  • The history sync takes a signal, so a lock or reset stops it writing.
  • Receive-history sync at startup and after payments is merged from feat(lnurl): attribute receives that landed while the wallet was closed #1082.
  • Arkade destinations must match the wallet's network/operator, and Bitcoin destinations must equal its boarding address.
  • The lightning address under the QR shows only on the Unified and Lightning views, and tapping it copies it.
  • Once a payment arrives, the screen stops asking for invoices. The payment listener writes the received amount into the flow, and the invoice hook used to re-quote it, leaving an unpaid hold invoice at the solver after every receive (seen on mainnet: a 1381-sat quote right after a 1381-sat payout).
  • Behaviour with 0.8.0.
    • The package runs claim and discovery one at a time per identity and server. A discovery for a new facade therefore waits for a claim still in flight and adopts it, rather than dropping it.
    • Such a claim keeps the destinations it was made with until feat(receive): one QR, a Showing selector and selectable part rows #1086's refresh re-binds them; funds still reach the wallet's own earlier address.
  • Configuration: VITE_LNURL_SERVER and optional VITE_LNURL_DOMAIN. Deployments still setting only the removed VITE_LNURL_SERVER_URL must rename it before rollout; docs/swaps.regtest.md explains how.

Token receive

  • Offer LNURL token rails only after both receive opt-ins and an amount are set, and quote only the selected option.
  • Use the package's requestTokenQuote, tokenOptionAccepts, normalization, URI validation and chainName.
  • Keep the wallet's policies:
    • canonical positive units;
    • refusal of a quote that requires a memo;
    • at least 60 seconds of quote life;
    • supported decimals;
    • the 0.8–1.25 USD price tolerance.
  • An expired or failed quote is explained where its code was, with a Get a new quote button there. An expired one says not to send to the old address. No other order is placed unasked.
  • A chosen stablecoin has a button beside it that goes back to the code shown before it.
  • Includes merged fix(receive): expire a stablecoin quote by the clock, and regenerate it on request #1085.
  • The token picker reads "Pay with stablecoins" and shows Tether over USDC until a token is picked. The logos are hidden from assistive tech, so the button's name stays its label.
  • The shared keypad keeps an in-progress amount through a background quote update.

Settings → Lightning address

  • Shows owned addresses, the server and receive limits (lnurlRailLimits).
  • Names a nameless receiver, including reserved-name claim codes. The page offers Claim name and Pick one for me. Claim name opens a sheet, where an "I have a claim code" link shows and hides the claim code field. A hidden code is not sent. On a server that offers claim codes but no self mode, where a chosen name needs a code, the sheet asks for the code first, above the name.
  • A reserved name's code claims that name on its own. Where a name needs a code, the name field is optional.
  • A code for any free name answers username_required. The sheet then asks for a name (lnurl-server 0.6.0, feat(addresses): claim codes for any free name, and a reserved name's code on its own ArkLabsHQ/lnurl-server#100).
  • A wallet that already has an address can still redeem a code. "I have a claim code" sits under its addresses. The claim registers a further address, which becomes the primary.
  • A "Primary address" picker shows when the wallet owns more than one active address. The choice is saved as lnurlPrimary and passed to owned({ handle }) by both Settings and Receive. A new choice switches the receiver in place.
  • A claim that registers its address but fails to bind it is followed by a best-effort owned({ refresh: true }), which binds anything left unbound. A wallet that had no address yet then adopts it.
  • A wallet that already holds a name is never offered "Pick one for me", here or on Receive. That includes a wallet whose current address is its nameless LNURL. It can still choose a name, or claim one with a code.
  • Holds Accept tokens, off by default, after the custody explainer. The Receive section hides when the server offers no token option.
  • Says why when the wallet's own addresses or a nameless receiver's domain cannot load.

Tests

  • Wallet tests cover wallet code and policy. @arkade-os/lnurl-client's own suite covers the package's behaviour (packages/client/test in ArkLabsHQ/lnurl-server).
  • Wallet tests that only re-checked package behaviour were removed, each with a counterpart there:
    • the confirmation and resolver copies;
    • watermark and sent-store parsing;
    • lnurl.test.ts and its fixtures;
    • the resolveLnurlRails boundary block;
    • the chain-label table.
  • The fixes in this PR were each reproduced by a failing test first. These include the bounds failures, the storage-shape crashes, the claim-fee wait, the Spanish-locale refusals, the missing-handle 404, the token retry, the range formatter, and the settings load errors.

Known gap on mainnet

Validation

  • At d210447d: TypeScript, lint and Prettier pass, and the full unit suite passes with 118 files / 1,273 tests. That includes @bordalix's TokenQuoteNote layout and lint commits, the Max fix and the receive re-quote fix, whose tests failed first. The CodeRabbit fixes and the expired-quote and back-button changes each have tests that failed first.
  • Every commit layer that used to be its own PR passes TypeScript, lint and Prettier.
  • The stablecoin picker was checked rendered in a browser, in dark and light themes.
  • Exact-head GitHub CI is tracked in the checks below.

No persisted schema migration. No merge or deployment is included.

Summary by CodeRabbit

New Features

  • Added Lightning address support for sending and receiving, including address setup, naming, and configurable settings.
  • Added token payment options with stablecoin quotes, network details, expiry handling, and token-receive controls.
  • Added LNURL payment confirmation tracking and activity updates, with payment details and confirmation status in transaction history.
  • Added branded QR codes, wallet-opening options, and payment method selection.
  • Added support for Lightning addresses in Bitcoin payment links.

Improvements

  • Improved amount validation, payment routing, localized error messages, and reset failure feedback.
  • Added English and Spanish translations for new payment flows and statuses.

Documentation

  • Updated deployment guidance for Lightning address server configuration.

Tests

  • Expanded coverage for LNURL sending, receiving, token payments, confirmations, settings, and transaction history.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Deploying wallet-bitcoin with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3f0a414
Status: ✅  Deploy successful!
Preview URL: https://d8cae7eb.wallet-bitcoin.pages.dev
Branch Preview URL: https://split-p12-settings-lnaddress.wallet-bitcoin.pages.dev

View logs

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The pull request adds LNURL receive and send flows with Lightning, Arkade, on-chain, and token rails. It adds receiver onboarding, token quotes, payment persistence, Lightning address settings, transaction metadata, confirmation status, deployment configuration, and tests.

Changes

LNURL payments

Layer / File(s) Summary
Configuration and settings
Dockerfile, docs/swaps.regtest.md, src/lib/i18n.ts, src/lib/types.ts, src/providers/*, src/screens/Settings/*
Adds LNURL server and domain configuration, Lightning address settings, token-receiving configuration, localized messages, and reset-error handling.
Receiver onboarding and token quotes
src/lib/receive/*, src/screens/Wallet/Receive/LnurlRail.tsx, src/test/lib/receive/*
Adds receiver discovery, onboarding, claims and upgrades, payment-rail resolution, token filtering, quote validation, expiry handling, and fake-server coverage.
Send routing and settlement
src/lib/sendRouter.ts, src/lib/lnurl.ts, src/screens/Wallet/Send/*, src/providers/swaps.tsx, src/test/screens/wallet/send*.tsx, src/test/lib/sendRouter.test.ts
Adds LNURL rail routing, payable-range validation, quote selection, target and amount checks, payment submission, confirmation watching, and optional swap-router creation.
Receive QR and token presentation
src/screens/Wallet/Receive/QrCode.tsx, src/components/ChainLogo.tsx, src/components/QrCode.tsx, src/components/Input*.tsx, src/test/screens/wallet/receive-lnurl.test.tsx
Adds Lightning and token payment selection, quote-aware QR and copy flows, token and chain logos, custom QR logos, and disabled recipient input behavior.
Payment persistence and wallet lifecycle
src/lib/lnurlActivitySync.ts, src/lib/lnurlPaymentRepository.ts, src/lib/lnurlSends.ts, src/providers/wallet.tsx, src/test/providers/*
Adds LNURL payment storage, activity synchronization, confirmation resumption, payment-triggered reloads, and reset cleanup ordering.
History and validation
src/lib/activityHistory.ts, src/lib/bip21.ts, src/lib/error.ts, src/lib/swapDisplay.ts, src/screens/Wallet/Transaction.tsx, src/components/Details.tsx, src/components/TransactionsList.tsx, src/test/lib/*, src/test/screens/wallet/transaction*.tsx
Adds LNURL metadata and labels, Lightning-address BIP21 handling, amount-range errors, receiver confirmation status, transaction details, and activity-history tests.

Priority: Normal

Estimated code review effort: 5 (Critical) | ~90 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Wallet
  participant LnurlRail
  participant LNURLServer
  participant SendRouter
  participant PaymentStore
  Wallet->>LnurlRail: Resolve receiver and payment rails
  LnurlRail->>LNURLServer: Request capabilities and pay data
  LNURLServer-->>LnurlRail: Return receivers and payment options
  Wallet->>SendRouter: Request LNURL quote
  SendRouter->>LNURLServer: Request payment quote
  LNURLServer-->>SendRouter: Return quote and settlement metadata
  Wallet->>PaymentStore: Record payment and confirmation data
  PaymentStore-->>Wallet: Return activity and confirmation state
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: bordalix

Merge Risk: Low · up to 217f9

After a wallet refresh, Lightning address setup may use an older address lookup. Guarding the lookup would prevent this edge case.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely summarizes the main changes: LNURL send and receive support, token rails, and the Lightning address settings page.
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.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Deploying wallet-mutinynet with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3f0a414
Status: ✅  Deploy successful!
Preview URL: https://5c49f884.arkade-wallet.pages.dev
Branch Preview URL: https://split-p12-settings-lnaddress.arkade-wallet.pages.dev

View logs

@arkana-ai-bot arkana-ai-bot 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.

Arkana review — feat(settings): Lightning address page

No protocol-critical paths touched (no VTXO handling, signing, forfeit, round lifecycle, or unilateral exit).


Correctness

lnurlRailLimits fallback (src/lib/receive/lnurlRail.ts, new +66–70)

min: Math.ceil((o.minSendable ?? payRequest.minSendable) / 1000),
max: Math.floor((o.maxSendable ?? payRequest.maxSendable) / 1000),

PaymentOption.minSendable/maxSendable are optional in the server schema (confirmed in lnurl-server/src/payment-options.ts). The fallback to the top-level payRequest fields is correct because LUD-06 requires those to always be present. Logic is sound.

server?.domain in yours TableData (src/screens/Settings/Lnurl.tsx, ~line 86)

[t('settings.lnurlServer'), server?.domain],

server is LnurlServer | null (return type of configuredLnurlServer()), so .domain is string | undefined here. The filter in Advanced.tsx guarantees the page is only mounted when server is truthy, so this is safe at runtime — but if TableData is typed as [string, string][], TypeScript will reject it; if it accepts string | undefined, a future path that bypasses the filter would silently render "undefined" in the UI. Consider asserting server!.domain (or narrowing once at the top of the component with an early return) to make the invariant explicit.

handleChange is async without await (src/screens/Settings/Lnurl.tsx, ~line 99)

const handleChange = async () => {
  backupAndUpdateConfig({ ... })
}

backupAndUpdateConfig is synchronous (its mock type is (config: Config) => void). The async keyword is harmless but dead weight — it silently swallows any future exception if backupAndUpdateConfig is made async later. Either drop async or add await and surface errors (the existing pattern in other settings handlers is sync).


Error handling

payRequest() failure has no UI surface (src/screens/Settings/Lnurl.tsx, ~lines 63–72)

rail.receiver
  .payRequest()
  .then(...)
  .catch(consoleError)   // logged, never shown

A transient network error on the lnurl server during settings load silently leaves the "What payers can send" section empty, with no indication of failure. rail.status === 'failed' covers registration/init failures but not this secondary fetch. A small error state wired to this catch would match the existing ErrorMessage pattern used for name-claim failures on the same page.


Dependencies

config.receiveViaLnurl and config.receiveViaTokens are used throughout Lnurl.tsx but do not appear in the Config type visible in this PR's src/lib/types.ts diff — they must be added by the dep chain (#1083). TypeScript build will hard-fail if this merges before #1083 lands; the test plan's tsc 0 check confirms the series is expected to be applied in order.


Test coverage

Nine tests cover the happy paths and the one-time token explainer flow well, including the "seen before / not seen" state. No test exercises rail.status === 'failed' rendering or a payRequest() rejection. Low risk for a settings display, but worth a follow-up if the empty-limits silent-failure above is addressed.


Privacy disclosure

Both warning strings are explicit and accurate:

  • receiveViaLnurlSubtext tells the user the server sees every payment when the toggle is on.
  • tokenExplainer names the third-party deposit holder and is shown exactly once before opt-in.

No concerns there.


Cross-repo

lnurlRailLimits is a new export from lnurlRail.ts but is wallet-internal (not re-exported from an SDK package). No consumers found in ts-sdk, go-sdk, rust-sdk, or dotnet-sdk. No cross-repo breakage.


Summary

Three items worth addressing before merge:

  1. Assert or narrow server before building yours (defensive ?. obscures a real invariant).
  2. Drop async from handleChange or add proper error handling.
  3. Consider surfacing payRequest() fetch errors in the UI.

All three are minor; none is a blocker if the series is landing together and the CI tsc gate is green.

@Kukks

Kukks commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

@arkana-ai-bot thanks. All three points are fixed in a01ce4ef:

  1. server?.domain: the server row is built only when a server is configured, the same way the LNURL and address rows above it are, so no undefined row can render.
  2. async without await: handleChange is a plain function now, like the other settings handlers.
  3. Silent payRequest failure: a failure now shows under "What payers can send" through ErrorMessage. A new test makes the fake server fail the payRequest and checks that the page says so and shows no limits.

On the dependency note: this PR is stacked on #1083, so the config fields are already in its tree.

@arkana-ai-bot

arkana-ai-bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Thanks @Kukks — verified all three in a01ce4ef:

  1. server?.domain — conditional spread ...(server ? [[..., server.domain]] : []) makes the invariant explicit at the type level; no optional chaining, no stale-path risk. ✅
  2. async handleChange — dropped to a plain sync function, consistent with the other settings handlers. ✅
  3. Silent payRequest failure — limitsError state wired into the catch, rendered via ErrorMessage inside the "What payers can send" section (shown even when sendable is empty), and a new test with payRequestFail: true confirms the error text surfaces and no limits appear. stale guard is correct. ✅

Nothing further from me on these items.

@arkana-ai-bot arkana-ai-bot 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.

Follow-up pass on 7508ca2 — 10 commits, 18 files since c7521dd. All 4 issues from the prior review are closed; no new blocking findings.


Prior findings — status

1. server?.domain optional chain in yours TableData (Settings/Lnurl.tsx)
Fixed in a01ce4ef. The spread now conditionally includes the row only when server is truthy and asserts the full [string, string] tuple — the TypeScript gap and the silent-undefined render are both gone.

2. handleChange declared async without await (Settings/Lnurl.tsx)
Fixed in a01ce4ef. async removed.

3. payRequest() rejection not surfaced in the UI (Settings/Lnurl.tsx)
Fixed in a01ce4ef. limitsError state is now set on the catch path and rendered via <ErrorMessage> under the limits section header. The section itself is now shown when either sendable.length or limitsError is truthy — correct. The test at settings/lnurl.test.tsx +104 ('says so when what payers can send cannot be read') locks the behaviour against regression.

4. No test for payRequest() failure path
Fixed — covered by the test noted above and by the payRequestFail option added to fakeLnurlServer.


New findings on the incremental diff

lnurlTokenRails.ts +109 — strict verifiability gate

- const offered = o.available && o.verifiable !== false && o.unit.decimals <= 18 && inBounds
+ const offered = o.available && o.verifiable === true && o.unit.decimals <= 18 && inBounds

Security improvement. The previous !== false would silently offer a token option whose verifiable field was absent (undefined), meaning the wallet would display a receive QR for a token it cannot confirm was actually paid. The strict === true is the right gate. Test added (['no verifiable flag', { verifiable: undefined }]) locks it.

lnurlRail.ts — guard own addresses before requesting arkade/onchain legs

const rails: LnurlRailType[] = [
  'lightning',
  ...(allRails && ownArkade ? (['arkade'] as const) : []),
  ...(allRails && ownOnchain ? (['onchain'] as const) : []),
]

Correct fix. Previously, resolveLnurlRails was called with empty strings for own.arkade/own.onchain when the wallet's addresses hadn't resolved yet, causing sameOperator / the onchain equality check to always fail on the first render. Now those legs are deferred until the address is known. The effect's dep array ([receiver, amountSat, allRails, ownArkade, ownOnchain]) re-fires when they arrive, so nothing is lost. The new useLnurlRails integration test pins the sequencing.

lnurlPaymentRepository.ts — db.close() on error paths

Three error handlers previously rejected without closing the connection; all three are fixed. An IndexedDB connection left open on an error path can block a future indexedDB.open() version-upgrade transaction, which would silently stall activity sync. Correct.

QrCode.tsx +588 — Copy disabled when token selected but quote absent

disabled={generating || Boolean(activeToken && !tokenQuote)}

generating (tokenRails.quoting) is false once a quote attempt resolves — whether successfully or with an error. In the error case tokenQuote is undefined, so without this extra condition a Copy press would fall through to tokenQuote?.destination ?? qrCodeValue and copy the wrong value (the invoice/address for a different rail). The added guard closes that window. Test in receive-lnurl.test.tsx +583 verifies the button is disabled when the error fires.

swapDisplay.ts — lnurlLabel now takes t: Translate

Pure i18n plumbing; all call-sites updated; Spanish strings present and tested. No issues.

LnurlRail.tsx — hardcoded strings removed

All UI strings now go through t(...). Both en and es keys are wired in i18n.ts. No issues.


Remaining concern (carry-over, unchanged)

The series dependency note from the prior review stands: config.receiveViaLnurl / config.receiveViaTokens must arrive from #1083 before this merges or tsc fails. No code change in this increment addresses or worsens that; it is purely a merge-order enforcement matter already called out in the PR description's test plan.


No protocol-critical paths (VTXO, signing, forfeit, round lifecycle, unilateral exit) are touched in this diff.

@arkana-ai-bot arkana-ai-bot 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.

Follow-up pass on 7508ca2 — 10 commits since c7521dd; all 3 prior actionable findings fixed, 1 prior note unchanged.


Prior findings: closure

1. server?.domain undefined in yours TableData — Fixed (commit a01ce4ef)
Replaced with a conditional spread ...(server ? [[t('settings.lnurlServer'), server.domain] as [string, string]] : []). Invariant now expressed in type, not just runtime guard.

2. handleChange spuriously async — Fixed (commit a01ce4ef)
Dropped async. Matches the sync pattern used by every other settings handler.

3. payRequest() failure silent in UI — Fixed (commit a01ce4ef)
limitsError state added, extractError imported, the .catch sets limitsError, and <ErrorMessage> renders it in the limits section. Test 'says so when what payers can send cannot be read' covers the path using payRequestFail on fakeLnurlServer.

4. Merge-order dependency on #1083 — Still open, no change
config.receiveViaLnurl/config.receiveViaTokens on Config still require #1083 in base first. Nothing here changes that.


Incremental findings

src/lib/receive/lnurlTokenRails.ts:109 — verifiable !== false → verifiable === true
An absent verifiable field previously allowed the option to be offered; it is now rejected. For a flow routing incoming payments through a third-party token deposit, requiring an explicit true is the safer default. Test updated to include 'no verifiable flag' as an omit case. No concerns.

src/lib/receive/lnurlRail.ts:135–146 — useLnurlRails deferred-rail guard
allRails && ownArkade / allRails && ownOnchain now gate each rail's inclusion. resolveLnurlRails rejects a resolved address that differs from the wallet's own, so passing an empty string (address not yet loaded) would always fail the foreign-address check. Effect re-runs when addresses load; the stale-flag cleanup is in place. New test useLnurlRails asks for a leg only once its own address is known pins the behavior. Correct fix.

src/lib/lnurlPaymentRepository.ts:40, 57, 73 — IDB db.close() on error paths
Three onerror handlers previously rejected without closing the connection; they now close it first. Prevents an IDB connection leak when a read, write, or clear operation fails. Good fix.

src/lib/swapDisplay.ts:107 — lnurlLabel(tx, t) signature change
lnurlLabel now takes t: Translate rather than producing hardcoded English. All callers updated: TransactionsList.tsx:113, swapDisplay.test.ts, activityHistory.test.ts. Spanish translation test added. This is wallet-internal; no cross-repo API surface affected.

src/screens/Wallet/Receive/QrCode.tsx:590 — Copy disabled when activeToken && !tokenQuote
Copy button is now disabled while a token quote is pending, preventing the user from copying an incomplete payment URI. Correct guard.

src/screens/Wallet/Receive/LnurlRail.tsx — String literals moved to i18n
NameForm and LnurlRailPanel now use t(...) throughout. The lnurl.test.tsx references 'Choose a name' / 'Claim code' as placeholder strings — these are the English values of the respective keys established in prior commits. Tests pass, keys resolve correctly.


No protocol-critical paths touched. No new security concerns. All three raised findings were resolved with test coverage. The merge-order note on #1083 stands unchanged. Ready to land once #1083 is in base.

@arkana-ai-bot arkana-ai-bot 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.

Follow-up review — 5 commits since 7508ca2, 4 of 5 prior issues fixed, 1 still open

This push merges three upstream split branches (p06, p09, p11) into p12 and adds a handful of targeted fixes across the receive rail. No protocol-critical paths (VTXO / signing / forfeit / round / unilateral exit) are touched.


Prior-issue closure

Fixed — server?.domain optional access in yours TableData (src/screens/Settings/Lnurl.tsx)
Now uses a guarded spread: ...(server ? [[t('settings.lnurlServer'), server.domain] as [string, string]] : []). The invariant is explicit and TypeScript-sound.

Fixed — async on handleChange without await (src/screens/Settings/Lnurl.tsx)
handleChange is now a plain synchronous arrow function.

Fixed — payRequest() failure silently lost (src/screens/Settings/Lnurl.tsx, ~line 69)
The .catch now calls setLimitsError(extractError(err)), and limitsError is wired to <ErrorMessage> inside the limits section. The gap I flagged is closed.

Fixed — receiveViaLnurl / receiveViaTokens missing from Config type
split/p11-token-rails is now in the branch base; the type is present and tsc passes.

Still open — no test for lnurlRail.status === 'failed' render path
The new tests cover stale-invoice suppression, multi-locale error messages, and generic fallback, but none mount the settings Lnurl page (or the receive QrCode) with status === 'failed' to verify the rail.error text is displayed. Low risk for a display-only path, but the gap persists as noted last pass.


New findings (incremental diff only)

useLayoutEffect for current.current — correct fix (src/lib/receive/lnurlRail.ts, lines 240-242)
Replacing the render-phase assignment with useLayoutEffect(() => { current.current = facade }, [facade]) is the right pattern for concurrent React. Layout effects commit synchronously before any user callbacks fire, so the ref is always consistent by the time run reads it. No concerns.

Empty-group vacuous-truth fix (src/lib/activityHistory.ts, line 274)
Adding activity.txs.length > 0 before the every(...) guard correctly prevents empty sent: groups from satisfying the skip condition vacuously. Clean.

Stale-invoice suppression (src/screens/Wallet/Receive/QrCode.tsx, line 153)

const invoice = (swapRailOn ? recvInfo.invoice : undefined) || lnurlRails.targets.lightning || ''

When the LNURL rail is active, a swap invoice that persisted in the flow context from a previous session is intentionally discarded. The loading state is correctly covered by lnurlRails.generating, and the new test at line 218 verifies the behaviour end-to-end. Good.

CLAIM_ERROR_KEYS typing — minor (src/lib/receive/lnurlRail.ts, line 183)
Record<string, string> types index access as string, not string | undefined, so TypeScript won't enforce the if (key) guard unless noUncheckedIndexedAccess is on. The runtime check is still there, so this is harmless, but Partial<Record<'taken' | 'invalid_username' | 'blacklisted' | 'invalid_claim' | 'forbidden_mode' | 'limit_reached', string>> (or a satisfies assertion) would make the exhaustiveness more explicit. Nit only.


Summary

The four substantive issues from the last review are fixed. The one remaining gap (test for the failed rail render) is low risk and can be a follow-up. The incremental changes are clean; the useLayoutEffect refactor and invoice-priority fix are both correct.

@Kukks

Kukks commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

@arkana-ai-bot thanks.

  • The open item: a test for the failed rail render is added in this push. With no owned address and a failing capabilities call, the page shows the error and offers no naming form.
  • The CLAIM_ERROR_KEYS typing nit: noted. That map lives in feat(receive): LNURL rails and background activity sync #1081, and I will fold Partial<Record<…>> in with its next change rather than push a typing-only change through the stack on its own. The runtime if (key) guard already covers the missing case.

@arkana-ai-bot

arkana-ai-bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Verified both points in a55a64e.

Failed rail test — capabilitiesFail: true makes the /lnurl/capabilities endpoint return 500 with { error: 'domain lookup failed' }. The test asserts the error text surfaces and queryByPlaceholderText('Choose a name') returns nothing. No owned address is passed (empty addresses), so the rail lands in failed with the server error rather than an address-resolution miss. Covers the gap. ✅

CLAIM_ERROR_KEYS nit — Deferral noted. The runtime if (key) guard is sufficient for now; folding Partial<Record<…>> into the next #1081 change is the right call.

No further open items on this PR.

@arkana-ai-bot arkana-ai-bot 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.

Follow-up review — 3 commits since c93d211, 1 remaining prior issue fixed, 0 still open

Commits: e7fa58c (URI param-injection fix + Share guard), c8f3753 (merge p11), a55a64e (failed-rail test). No protocol-critical paths (VTXO / signing / forfeit / round / unilateral exit) touched.


Prior-issue closure

Fixed — no test for lnurlRail.status === 'failed' render path
a55a64e adds 'says why the lightning address could not be loaded, and offers no naming' in src/test/screens/settings/lnurl.test.tsx. The capabilitiesFail: true flag makes the fake server return a 500 with { error: 'domain lookup failed' }; the test asserts the error text is visible and the naming form is absent. Closes the gap. ✅

CLAIM_ERROR_KEYS typing nit — still deferred to the next #1081 change as agreed. Not this review's subject.


New findings (incremental diff only)

src/lib/receive/lnurlTokenRails.ts:47–52 — security fix for query-parameter shadowing on payment URIs

const only = (key: string) => {
  const values = q.getAll(key)
  return values.length === 1 ? values[0] : undefined
}

Previously q.get(key) was used for EVM (address, uint256) and Solana (spl-token, amount) validation. URLSearchParams.get() returns the first value when a key is repeated. The new only() helper returns undefined for any multi-valued key, causing paysQuote to return false and the UI to fall back to the raw deposit address. The vulnerability class is query-parameter shadowing / HTTP parameter pollution on payment URIs; the impact class is misdirected token payment. The fix is correct and complete: rejecting ambiguous URIs outright is the right policy. Both new test cases ('a second, conflicting recipient', 'the amount repeated') cover EVM.

Minor: the Solana duplicate-key cases (spl-token, amount repeated) are not exercised by a test, but the code path is identical to the EVM path through only(). Low risk; a follow-up test case covering only('amount') with a duplicated Solana amount param would complete coverage.

src/lib/receive/lnurlTokenRails.ts:67 — decimal(only('amount') ?? null) is consistent

decimal() was already called with a null return from q.get() in the original code; the ?? null coercion maintains that contract. No behavioural change when the key is absent or single-valued.

src/screens/Wallet/Receive/QrCode.tsx:413 — Share disabled guard matches Copy

Boolean(activeToken && !tokenQuote)

Symmetry with the existing Copy button guard is correct. The new test in src/test/screens/wallet/receive-lnurl.test.tsx (Share mock + disabled assertion when token error is present) pins the behaviour. ✅

src/test/screens/wallet/receive-lnurl.test.tsx:56–60 — onTestFinished cleanup

share.can = false is reset via onTestFinished rather than afterEach, which is the right scoping (test-local cleanup rather than suite-wide). The mock is not leaked across tests. ✅


Summary

All prior open items are now closed. The only() security fix is the most material change — correct approach, good test coverage for EVM, Solana coverage optional as a follow-up. No blocking issues.

@arkana-ai-bot arkana-ai-bot 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.

Arkana follow-up — a55a64e → 0adb038

Status: 3 commits landed since last pass (1 content + 2 merges). Prior issues: 0 fixed, 3 still open, 0 no longer applicable. No new findings in the incremental diff.


What changed

c5f2fd3 — the only substantive commit; two changes:

src/lib/receive/lnurlRail.ts line 177 — CLAIM_ERROR_KEYS promoted from Record<string, string> to Partial<Record<string, string>>.
This is correct. Without noUncheckedIndexedAccess, the non-Partial type told TypeScript any string key was always present, silently handing a non-null string to t() even for unknown error codes. The call site already has if (key) return t(key) (line 191), so the guard was defensive but not compiler-enforced; now it is. No behaviour change, improved type accuracy. ✓

src/test/lib/receive/useLnurlRail.test.ts — new test: "settles back on the same receiver when the boarding address arrives after a session registered."
The test correctly pins that result.current.lnurl is reference-equal across the re-render, not just truthy — which matters here because a changed LNURL would invalidate any QR the user had already shared. Also checks error === '' after re-render. Both assertions are the right ones to have. ✓


Prior-issue status

  1. server?.domain without assertion (src/screens/Settings/Lnurl.tsx, ~line 86) — Still open. File untouched in this diff.

  2. handleChange is async without await (src/screens/Settings/Lnurl.tsx, ~line 99) — Still open. File untouched in this diff. Low risk now, but a future async backupAndUpdateConfig would silently swallow errors.

  3. payRequest() failure has no UI surface (src/screens/Settings/Lnurl.tsx, ~lines 63–72) — Still open. The rail.status === 'failed' rendering test added at a55a64e covers the rail itself failing; a transient payRequest() error (the secondary fetch that populates the "What payers can send" limits) is a distinct path and still surfaces nothing to the user.


No protocol-critical paths touched. Incremental changes are clean; three pre-existing nits in Lnurl.tsx remain but are cosmetic/low-risk for a settings display screen.

@Kukks

Kukks commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@arkana-ai-bot arkana-ai-bot 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.

Follow-up pass on 3219ebf — 5 commits, 6 files since 0adb038. All prior actionable findings remain Fixed; 1 standing note unchanged. No new blocking issues.


Prior findings — closure

1. server?.domain — Fixed (a01ce4ef)
2. async handleChange — Fixed (a01ce4ef)
3. payRequest() silent failure — Fixed (a01ce4ef)
4. Merge-order dependency on #1083 — Still open, unchanged.
config.receiveViaLnurl/config.receiveViaTokens still require #1083 in base. Nothing in this increment changes or worsens that; the tsc 0 gate in the test plan is the enforcement mechanism.


Incremental diff findings

src/lib/lnurlPaymentRepository.ts — write-gate and clearLnurlPaymentsForReset

Logic is correct. The key invariant the comment claims — "Refusing and registering happen in one synchronous turn, so no write can slip past closeWrites and then land after writesSettled resolved" — holds. Between the gate check (if (closed) return) and the inFlight.add(write), there is no await; JavaScript's single-threaded event loop guarantees no other code can run in that window. A write that passes the gate is in inFlight before closeWrites() can observe it, and writesSettled() spreads inFlight after closed is already true, so no new writes can enter after the snapshot. ✅

The finally { inFlight.delete(write) } cleanup is safe: Promise.allSettled already holds a reference to the promise, so removing it from inFlight after settlement doesn't affect the outstanding allSettled call. ✅

writesSettled() on rejection path (lnurlPaymentRepository.test.ts +160): the finally block runs and removes the promise from inFlight before writesSettled() is called in the test, so allSettled([]) resolves immediately. Correct. ✅

src/providers/wallet.tsx — ordering change: lnurl clear is now fatal-before-destructive

// before
await clearStorage()
...
await lnurlPaymentRepository.clear().catch(consoleError)   // non-fatal, after destructive

// after
await clearLnurlPaymentsForReset()   // fatal, before destructive
await clearStorage()
...

Moving this before clearStorage() and making it fatal is the right call. These records hold preimages; a completed reset that left them intact would hand the next wallet the previous wallet's payment history. If the clear fails (e.g., IDB locked by a second tab), the reset now aborts before anything is destroyed, and the wallet is left intact. The error path surfaces correctly in the UI (see below). ✅

One edge case worth noting but not a blocker: if clearLnurlPaymentsForReset() succeeds but a later step (e.g., svcWallet.clear()) fails, the records are gone and the reset aborts without a page reload. On the next retry, closeWrites() is idempotent on the singleton, writesSettled() resolves immediately (empty inFlight), and clear() runs on an already-empty store — so retry is safe. ✅

src/screens/Settings/Reset.tsx — error feedback

setError('') at the top of handleReset clears stale errors before each attempt. The fixed resetFailed string deliberately avoids echoing the underlying error, which could name internal storage objects or paths. The Reset.test.tsx test asserts not.toHaveTextContent('indexeddb unavailable') to lock this. setLoading(false) resets only in the catch path; on success location.reload() obviates any state cleanup. ✅

src/lib/i18n.ts — resetFailed string pair

Both the English and Spanish strings name the symptom ("nothing was cleared") and the mitigation ("close other tabs"), without identifying which subsystem failed. Good. ✅

src/lib/lnurlPaymentRepository.ts — readWatermarkMap validation (+170-172)

const parsed: unknown = JSON.parse(value)
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, number>) : {}

Defensive against null, [], and primitives in localStorage. The test covers 'null', '[]', and '7' as raw stored values and confirms both that the watermark reads as undefined and that a subsequent write succeeds. ✅

src/test/lib/lnurlPaymentRepository.test.ts — heldWriteStore helper

The tick() helper (setTimeout(resolve, 0)) correctly schedules a macrotask, which allows microtasks (Promise callbacks) to drain first. The test at +145 verifies settled === false before release(), and settled === true after. The sequencing is sound. ✅

src/test/screens/settings/reset.test.tsx — new file

Tests the two paths: failure (error message shown, underlying error not leaked, button remains enabled) and no-failure (no error message). Both are correct and cover the added code. ✅


No protocol-critical paths (VTXO handling, signing, forfeit, round lifecycle, unilateral exit) are touched in this diff. No new security concerns.

@arkana-ai-bot

arkana-ai-bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

You're right, @Kukks — that was my error again.

I've verified at HEAD (81f96b5c): src/test/screens/wallet/receive-lnurl.test.tsx:198 — "falls back to the swap rail when the configured lnurl-server is unreachable" — stubs fetch to throw a TypeError, which drives the capabilities call to failure and lands the rail in status === 'failed'. Line 209 asserts /^Lightning address unavailable: / is in the document, which exercises the LnurlRailPanel failed-state render path in QrCode.tsx. The test was added in 59fc2f2b and is unchanged.

My previous review at pullrequestreview-5672252582 correctly recorded this as Fixed. I should not have re-listed it as open in the 81f96b5c pass. Finding closed — no open items remain from my side.

…y the address from Receive

Claim sheet: on a server that offers claim codes but no `self` mode, a chosen
name cannot be claimed without a code. The sheet now asks for the code first,
above the name field, instead of hiding it behind the "I have a claim code"
link. Where the code is optional, the link still shows and hides the field.

Receive: the lightning address under the QR now shows only on the Unified and
Lightning views, which are the ones it pays, and tapping it copies it, the way
other copyable text in the wallet does.

@arkana-ai-bot arkana-ai-bot 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.

Follow-up pass on 27203da — 1 commit since 81f96b5; 0 prior findings fixed (none were open blockers), 2 low-risk carry-overs unchanged.


Prior-finding closure

CLAIM_ERROR_KEYS typed as Record<string, string> (src/lib/receive/lnurlRail.ts) — Still open, not worsened.
Unchanged from last pass. The runtime guard is present; this remains a nit.

No test for lnurlRail.status === 'failed' render path — Still open, not worsened.
Unchanged. Still low risk for a display-only path.

Neither was flagged as a blocker. This commit does not touch either site.


Incremental findings

NameForm — codeFirst logic (src/screens/Wallet/Receive/LnurlRail.tsx)

The three-way condition is correct for all choices combinations:

self admin codeFirst code = canClaim condition
✓ ✓ false claimCode if hasCode, else "" Boolean(username) (code optional)
✓ ✗ false "" always Boolean(username) (no code path)
✗ ✓ true claimCode always Boolean(username) && Boolean(code) (both required)

admin && !codeFirst correctly gates the "I have a claim code" link — it appears only when code is optional (self && admin), and is hidden when code is mandatory (admin && !self). The {hasCode && !codeFirst ? codeInput : null} guard prevents a duplicate field from rendering in the mandatory case. No logic gaps found.

codeInput is defined once and conditionally placed either at the top (mandatory) or below the name (optional, toggled by link). The focus prop on codeInput is appropriate in the mandatory case where it is the first field; in the optional-and-revealed case it still auto-focuses the revealed field, which is reasonable.

LnurlRailPanel — showAddress prop (src/screens/Wallet/Receive/LnurlRail.tsx:14, 26)

showAddress={activeMethod === 'unified' || activeMethod === 'lightning'} is the right predicate: a Lightning address is payable only on the Lightning and Unified rails; showing it under Arkade or Bitcoin would invite misdirected payments. Returning null on other methods (rather than hiding via CSS) is preferable — no node in the DOM for screen-readers to announce. Correct.

<Text centered copy={address}>{address}</Text> — the copy prop triggers clipboard write on tap, consistent with how other copyable values in the wallet are rendered.

Test coverage (src/test/screens/wallet/receive-lnurl.test.tsx:261)

New test covers: initial display on Unified, copy-on-tap, hidden on Arkade, visible again on Lightning. Sufficient for the surface changed. The Bitcoin/onchain method is not explicitly tested but follows the same branch as Arkade (!== 'unified' && !== 'lightning'); a follow-up is welcome but not blocking.

claimInSheet in src/test/screens/settings/lnurl.test.tsx is correctly updated: getAllByRole('textbox') returns [codeField, nameField] in DOM order, which now matches the rendered order for codeFirst scenarios (['admin', 'random'], ['admin', 'session']). Assertions on both placeholder values lock the ordering.


Summary

Clean single-commit delta. The codeFirst branching is correct, the showAddress guard is correct, and both paths have targeted test coverage. No protocol paths touched. No security concerns. The two lingering nits from prior passes are unaffected.

On @arkade-os/lnurl-client 0.9.0 and lnurl-server 0.6.0, a claim code can
carry its own name:

- On a server where a chosen name needs a code, the name field is now
  optional. A reserved name's code claims that name on its own. A code for any
  free name answers username_required, which the sheet explains: add a name
  and claim again.
- A wallet that already has a lightning address can still redeem a code. Its
  addresses list offers "I have a claim code", and the claim registers a
  further address, since an upgrade only renames a nameless one. The sheet
  closes once the address changes and stays open with the reason when a claim
  is refused.

The client bump is otherwise a drop-in: 0.9.0 adds registered methods and
receive amounts, which the wallet does not use, and a facade given no
methods never touches them.
Kukks added 3 commits October 28, 2026 12:44
…hat failed half way

On @arkade-os/lnurl-client 0.9.1, whose owned({ handle }) returns a given
active address:

- Settings → Lightning address lists a "Primary address" picker when the
  wallet owns more than one active address. The choice is saved in the
  config as lnurlPrimary, and both Settings and Receive pass it to
  owned({ handle }), so Receive uses it straight away and after a restart.
  A new choice switches the receiver in place, without reloading the rail.
- An address claimed with a code by an already named wallet becomes the
  primary, so Receive no longer falls back to the older one (CodeRabbit).
- A claim can register its address and then fail to bind the identity,
  leaving the address with no Arkade destination. After a failed claim, the
  rail now runs a best-effort owned({ refresh: true }), which binds whatever
  was left unbound (CodeRabbit).
- The username_required test goes on to add a name and claim (Arkana).

The picker hides while the claim sheet is open, because Select reads arrow
keys page-wide and the claim code field would feed it.
feat(lnurl): claim with a code alone or when already named, and choose the primary address
The rail can hold a nameless receiver while the identity owns a named
address too: owned() picks the session address first, and the user can
pick the nameless LNURL as primary. Settings and Receive then treated the
wallet as unnamed and offered "Pick one for me".

The rail now reports ownsName: the receiver's own name, or, when the
receiver is nameless, any active named address in the identity's list,
which it reads once. Both screens drop the random choice while it is true.
Choosing a name, or claiming one with a code, stays available.

@arkana-ai-bot arkana-ai-bot 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.

Follow-up pass — 2 substantive commits (aa06628, f520f1c) + 1 merge since 27203da. 0 prior findings newly fixed this push (all were already resolved); 1 prior finding still open; 1 minor new finding.

No protocol-critical paths (VTXO, signing, forfeit, round lifecycle, unilateral exit) are touched in this diff.


Prior-finding closure

1. server?.domain optional access — Fixed (a01ce4ef, prior passes). Conditional spread unchanged.

2. handleChange spuriously async — Fixed (a01ce4ef, prior passes). Unchanged.

3. payRequest() failure silently lost — Fixed (a01ce4ef, prior passes). Unchanged.

4. receiveViaLnurl/receiveViaTokens missing from Config — Fixed (p11 merge, prior passes). Unchanged.

5. CLAIM_ERROR_KEYS typing nit — Fixed (c5f2fd3, prior pass). Unchanged; Partial<Record<...>> now in place, and username_required has been added to it this push without reopening the gap.

6. No test for lnurlRail.status === 'failed' render path in QrCode.tsx — Still open. Nothing in this increment touches that path. Carried forward unchanged.


Incremental findings

aa06628 + f520f1c — claim-code-alone, second-address claim, primary selection

src/screens/Wallet/Receive/LnurlRail.tsx — canClaim logic change (line ~63)

Correct. The old guard required a username in every path, including admin-only where the code alone may carry the reserved name. The new guard accepts a code alone for admin mode (codeFirst = admin && !self). In self-mode, username is still required. In self+admin mode, either satisfies the guard, and a code-only submission gracefully receives username_required back from the server with a user-facing message — the two-step flow is tested in 'asks for a name when the code is one for any free name'. Logic is sound.

src/screens/Wallet/Receive/LnurlRail.tsx — onClick cleanup (line ~102)

Correct. The old form always sent username (potentially empty string) even in code-only mode. The new form omits absent fields. Test at 'claims a reserved name with its code alone' asserts the request body lacks a username key when only the code is provided. ✓

src/lib/receive/lnurlRail.ts:311 — bind-recovery result silently dropped (minor)

When a bind fails, facade.owned({ refresh: true }) is fired in the background to complete the bind — tested and confirmed working ('binds an address its claim registered but failed to bind' checks 2 calls to the bind endpoint). The recovery is correct server-side, but the resolved value is not consumed: there is no .then((next) => next && setReceiver(next)). As a result, the wallet stays in the error/onboarding state for the rest of the session even after the silent recovery succeeds; the user must navigate away and back to see their address.

Not a correctness or security issue — the bind does complete — but the UX leaves the user on an error screen after a problem that has already been resolved. Compare with the primary-switch effect, which does wire .then((next) => !stale && next && setReceiver(next)). Wiring the refresh result to setReceiver in the same pattern would close the gap. Low priority given how rare a partial bind failure is, but worth a follow-up before this path becomes more common.

src/lib/receive/lnurlRail.ts:257-268 — primary-switch useEffect (correct, note only)

The second effect intentionally excludes receiver from its dep array (eslint-disable comment explains why: adding it would cause the effect to re-run on every receiver update, creating loops). The stale flag is in place and correctly prevents stale setter calls. The guard receiver.handle === primary reads the closure value of receiver, which is the value from the last render that caused the dep array to change — that is the correct value to compare against when deciding whether a switch is needed. Logic is sound.

src/screens/Settings/Lnurl.tsx — primary saved only after a successful second-address claim (correct)

lnurlPrimary is written only when claimed is a Receiver (not undefined) and the wallet already had a named address. A username_required rejection returns undefined → no save. A complete failure returns undefined → no save. The useEffect(() => setClaiming(false), [rail.receiver?.lightningAddress]) sheet-close hook fires on address change, so a successful claim closes the sheet; a rejected one leaves it open with the error. Correct.

src/screens/Settings/Lnurl.tsx — Select hidden during claim (correct)

The {active.length > 1 && !claiming ? ... : null} guard is well-motivated: the Select component captures arrow-key events page-wide, which would interfere with claim-code and username fields inside the sheet. Good defensive UX.

@arkade-os/lnurl-client 0.8.0 → 0.9.1

Minor version bump; peer-dependency constraint (>=0.5.0-rc.0) unchanged. The new API surface (owned({ handle }), owned({ refresh: true }), username_required error code) is exactly what this push exercises. No cross-repo API surface exposed.


Summary

Two substantive commits land cleanly: claim-code-alone UX works correctly, the second-address claim flow is well-guarded, and primary address selection is correctly persisted. One minor follow-up: wire the bind-recovery result in claim() to setReceiver so a silently-recovered address surfaces in the current session without requiring navigation. The long-standing gap for status === 'failed' rendering in QrCode.tsx remains open but unchanged.

@arkana-ai-bot arkana-ai-bot 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.

Follow-up pass — 2 substantive commits (aa06628, f520f1c) + 1 merge since 27203da. 0 prior findings newly fixed this push (all were already resolved); 1 prior finding still open; 1 minor new finding.

No protocol-critical paths (VTXO, signing, forfeit, round lifecycle, unilateral exit) are touched in this diff.


Prior-finding closure

1. server?.domain optional access — Fixed (a01ce4ef, prior passes). Conditional spread unchanged.

2. handleChange spuriously async — Fixed (a01ce4ef, prior passes). Unchanged.

3. payRequest() failure silently lost — Fixed (a01ce4ef, prior passes). Unchanged.

4. receiveViaLnurl/receiveViaTokens missing from Config — Fixed (p11 merge, prior passes). Unchanged.

5. CLAIM_ERROR_KEYS typing nit — Fixed (c5f2fd3, prior pass). Partial<Record<...>> in place; username_required added to it this push without reopening the gap.

6. No test for lnurlRail.status === 'failed' render path in QrCode.tsx — Still open. Nothing in this increment touches that path. Carried forward unchanged.


Incremental findings

aa06628 + f520f1c — claim-code-alone, second-address claim, primary selection

src/screens/Wallet/Receive/LnurlRail.tsx — canClaim logic rewrite (line ~63)

Old: const canClaim = Boolean(username) && (self || Boolean(code))
New: const canClaim = Boolean(code) || (self && Boolean(username))

Correct. The old guard required a username in every path, including admin-only where the code alone may carry the reserved name. The new guard accepts a code alone for admin mode (codeFirst = admin && !self). In self-mode, username is still required. In self+admin mode, either satisfies the guard, and a code-only submission gracefully receives username_required back from the server with a user-facing message — the two-step flow is tested in 'asks for a name when the code is one for any free name'. Logic is sound.

src/screens/Wallet/Receive/LnurlRail.tsx — onClick omits empty fields (line ~102)

Old form sent username (potentially the empty string) even in code-only mode. New form uses ...(username && { username }) / ...(code && { claimCode: code }) to omit absent fields. Test at 'claims a reserved name with its code alone' asserts the request body lacks a username key when only the code is provided. ✓

src/lib/receive/lnurlRail.ts:311 — bind-recovery result silently dropped (minor)

When a bind fails, facade.owned({ refresh: true }) is fired as a background recovery — tested and confirmed that the second bind call reaches the server ('binds an address its claim registered but failed to bind' checks 2 calls to the bind endpoint). The recovery is correct server-side, but the resolved value is not consumed: there is no .then((next) => next && setReceiver(next)). As a result the wallet stays in the error/onboarding state for the rest of the session even after the silent recovery succeeds; the user must navigate away and back to see their address.

Not a correctness or security issue — the bind does complete — but the UX leaves the user on an error screen after a problem that has already been resolved. Compare with the primary-switch effect, which does wire .then((next) => !stale && next && setReceiver(next)). Wiring the refresh result to setReceiver in the same pattern would close the gap. Low priority given how rare a partial bind failure is, but worth a follow-up.

src/lib/receive/lnurlRail.ts:257-268 — primary-switch useEffect (correct, note only)

The second effect intentionally excludes receiver from its dep array (eslint-disable comment explains: adding it would cause re-runs on every receiver update, creating loops). The stale flag is in place and correctly prevents stale setter calls. The guard receiver.handle === primary reads the closure value of receiver from the last render that changed the deps — the right value to compare when deciding whether a switch is needed. Logic is sound.

src/screens/Settings/Lnurl.tsx — primary saved only after a successful second-address claim (correct)

lnurlPrimary is written only when claimed is a Receiver (not undefined) and the wallet already had a named address. A username_required rejection returns undefined → no save. A complete failure returns undefined → no save. The useEffect(() => setClaiming(false), [rail.receiver?.lightningAddress]) hook fires on address change, so a successful claim closes the sheet; a rejected one leaves it open with the error. ✓

src/screens/Settings/Lnurl.tsx — Select hidden during claim (correct)

The active.length > 1 && !claiming guard is well-motivated: the Select component captures arrow-key events page-wide, which would interfere with claim-code and username fields inside the sheet. Good defensive UX.

@arkade-os/lnurl-client 0.8.0 → 0.9.1

Minor version bump; peer-dependency constraint unchanged. The new API surface (owned({ handle }), owned({ refresh: true }), username_required error code) is exactly what this push exercises. No cross-repo API surface exposed.


Summary

Two substantive commits land cleanly: claim-code-alone UX is correct, the second-address claim flow is well-guarded, and primary address selection is correctly persisted. One minor follow-up: wire the bind-recovery result in claim() to setReceiver so a silently-recovered address surfaces in the current session without requiring navigation (src/lib/receive/lnurlRail.ts:311). The long-standing gap for status === 'failed' rendering in QrCode.tsx remains open but unchanged.

@arkana-ai-bot arkana-ai-bot 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.

Follow-up pass — 2 substantive commits (aa06628, f520f1c) + 1 merge since 27203da. 0 prior findings newly fixed this push (all were already resolved); 1 prior finding still open; 1 minor new finding.

No protocol-critical paths (VTXO, signing, forfeit, round lifecycle, unilateral exit) are touched in this diff.


Prior-finding closure

1. server?.domain optional access — Fixed (a01ce4ef, prior passes). Conditional spread unchanged.

2. handleChange spuriously async — Fixed (a01ce4ef, prior passes). Unchanged.

3. payRequest() failure silently lost — Fixed (a01ce4ef, prior passes). Unchanged.

4. receiveViaLnurl/receiveViaTokens missing from Config — Fixed (p11 merge, prior passes). Unchanged.

5. CLAIM_ERROR_KEYS typing nit — Fixed (c5f2fd3, prior pass). Partial<Record<...>> in place; username_required added to it this push without reopening the gap.

6. No test for lnurlRail.status === 'failed' render path in QrCode.tsx — Still open. Nothing in this increment touches that path. Carried forward unchanged.


Incremental findings

aa06628 + f520f1c — claim-code-alone, second-address claim, primary selection

src/screens/Wallet/Receive/LnurlRail.tsx — canClaim logic rewrite (line ~63)

Old: const canClaim = Boolean(username) && (self || Boolean(code))
New: const canClaim = Boolean(code) || (self && Boolean(username))

Correct. The old guard required a username in every path, including admin-only where the code alone may carry the reserved name. The new guard accepts a code alone for admin mode (codeFirst = admin && !self). In self-mode, username is still required. In self+admin mode, either satisfies the guard, and a code-only submission gracefully receives username_required back from the server with a user-facing message — the two-step flow is tested in 'asks for a name when the code is one for any free name'. Logic is sound.

src/screens/Wallet/Receive/LnurlRail.tsx — onClick omits empty fields (line ~102)

Old form sent username (potentially the empty string) even in code-only mode. New form uses ...(username && { username }) / ...(code && { claimCode: code }) to omit absent fields. Test at 'claims a reserved name with its code alone' asserts the request body lacks a username key when only the code is provided. ✓

src/lib/receive/lnurlRail.ts:311 — bind-recovery result silently dropped (minor)

When a bind fails, facade.owned({ refresh: true }) is fired as a background recovery — tested and confirmed that the second bind call reaches the server ('binds an address its claim registered but failed to bind' checks 2 calls to the bind endpoint). The recovery is correct server-side, but the resolved value is not consumed: there is no .then((next) => next && setReceiver(next)). As a result the wallet stays in the error/onboarding state for the rest of the session even after the silent recovery succeeds; the user must navigate away and back to see their address.

Not a correctness or security issue — the bind does complete — but the UX leaves the user on an error screen after a problem that has already been resolved. Compare with the primary-switch effect, which does wire .then((next) => !stale && next && setReceiver(next)). Wiring the refresh result to setReceiver in the same pattern would close the gap. Low priority given how rare a partial bind failure is, but worth a follow-up.

src/lib/receive/lnurlRail.ts:257-268 — primary-switch useEffect (correct, note only)

The second effect intentionally excludes receiver from its dep array (eslint-disable comment explains: adding it would cause re-runs on every receiver update, creating loops). The stale flag is in place and correctly prevents stale setter calls. The guard receiver.handle === primary reads the closure value of receiver from the last render that changed the deps — the right value to compare when deciding whether a switch is needed. Logic is sound.

src/screens/Settings/Lnurl.tsx — primary saved only after a successful second-address claim (correct)

lnurlPrimary is written only when claimed is a Receiver (not undefined) and the wallet already had a named address. A username_required rejection returns undefined → no save. A complete failure returns undefined → no save. The useEffect(() => setClaiming(false), [rail.receiver?.lightningAddress]) hook fires on address change, so a successful claim closes the sheet; a rejected one leaves it open with the error. ✓

src/screens/Settings/Lnurl.tsx — Select hidden during claim (correct)

The active.length > 1 && !claiming guard is well-motivated: the Select component captures arrow-key events page-wide, which would interfere with claim-code and username fields inside the sheet. Good defensive UX.

@arkade-os/lnurl-client 0.8.0 → 0.9.1

Minor version bump; peer-dependency constraint unchanged. The new API surface (owned({ handle }), owned({ refresh: true }), username_required error code) is exactly what this push exercises. No cross-repo API surface exposed.


Summary

Two substantive commits land cleanly: claim-code-alone UX is correct, the second-address claim flow is well-guarded, and primary address selection is correctly persisted. One minor follow-up: wire the bind-recovery result in claim() to setReceiver so a silently-recovered address surfaces in the current session without requiring navigation (src/lib/receive/lnurlRail.ts:311). The long-standing gap for status === 'failed' rendering in QrCode.tsx remains open but unchanged.

@arkana-ai-bot

Copy link
Copy Markdown
Contributor

Follow-up pass — 2 substantive commits (aa06628, f520f1c) + 1 merge since 27203da. 0 prior findings newly fixed this push (all were already resolved); 1 prior finding still open; 1 minor new finding.

No protocol-critical paths (VTXO, signing, forfeit, round lifecycle, unilateral exit) are touched in this diff.


Prior-finding closure

1. server?.domain optional access — Fixed (a01ce4ef, prior passes). Conditional spread unchanged.

2. handleChange spuriously async — Fixed (a01ce4ef, prior passes). Unchanged.

3. payRequest() failure silently lost — Fixed (a01ce4ef, prior passes). Unchanged.

4. receiveViaLnurl/receiveViaTokens missing from Config — Fixed (p11 merge, prior passes). Unchanged.

5. CLAIM_ERROR_KEYS typing nit — Fixed (c5f2fd3, prior pass). Partial<Record<...>> in place; username_required added to it this push without reopening the gap.

6. No test for lnurlRail.status === 'failed' render path in QrCode.tsx — Still open. Nothing in this increment touches that path. Carried forward unchanged.


Incremental findings

aa06628 + f520f1c — claim-code-alone, second-address claim, primary selection

src/screens/Wallet/Receive/LnurlRail.tsx — canClaim logic rewrite (line ~63)

Old: const canClaim = Boolean(username) && (self || Boolean(code))
New: const canClaim = Boolean(code) || (self && Boolean(username))

Correct. The old guard required a username in every path, including admin-only where the code alone may carry the reserved name. The new guard accepts a code alone for admin mode (codeFirst = admin && !self). In self-mode, username is still required. In self+admin mode, either satisfies the guard, and a code-only submission gracefully receives username_required back from the server with a user-facing message — the two-step flow is tested in 'asks for a name when the code is one for any free name'. Logic is sound.

src/screens/Wallet/Receive/LnurlRail.tsx — onClick omits empty fields (line ~102)

Old form sent username (potentially the empty string) even in code-only mode. New form uses ...(username && { username }) / ...(code && { claimCode: code }) to omit absent fields. Test at 'claims a reserved name with its code alone' asserts the request body lacks a username key when only the code is provided. ✓

src/lib/receive/lnurlRail.ts:311 — bind-recovery result silently dropped (minor)

When a bind fails, facade.owned({ refresh: true }) is fired as a background recovery — tested and confirmed that the second bind call reaches the server ('binds an address its claim registered but failed to bind' checks 2 calls to the bind endpoint). The recovery is correct server-side, but the resolved value is not consumed: there is no .then((next) => next && setReceiver(next)). As a result the wallet stays in the error/onboarding state for the rest of the session even after the silent recovery succeeds; the user must navigate away and back to see their address.

Not a correctness or security issue — the bind does complete — but the UX leaves the user on an error screen after a problem that has already been resolved. Compare with the primary-switch effect, which does wire .then((next) => !stale && next && setReceiver(next)). Wiring the refresh result to setReceiver in the same pattern would close the gap. Low priority given how rare a partial bind failure is, but worth a follow-up.

src/lib/receive/lnurlRail.ts:257-268 — primary-switch useEffect (correct, note only)

The second effect intentionally excludes receiver from its dep array (eslint-disable comment explains: adding it would cause re-runs on every receiver update, creating loops). The stale flag is in place and correctly prevents stale setter calls. The guard receiver.handle === primary reads the closure value of receiver from the last render that changed the deps — the right value to compare when deciding whether a switch is needed. Logic is sound.

src/screens/Settings/Lnurl.tsx — primary saved only after a successful second-address claim (correct)

lnurlPrimary is written only when claimed is a Receiver (not undefined) and the wallet already had a named address. A username_required rejection returns undefined → no save. A complete failure returns undefined → no save. The useEffect(() => setClaiming(false), [rail.receiver?.lightningAddress]) hook fires on address change, so a successful claim closes the sheet; a rejected one leaves it open with the error. ✓

src/screens/Settings/Lnurl.tsx — Select hidden during claim (correct)

The active.length > 1 && !claiming guard is well-motivated: the Select component captures arrow-key events page-wide, which would interfere with claim-code and username fields inside the sheet. Good defensive UX.

@arkade-os/lnurl-client 0.8.0 → 0.9.1

Minor version bump; peer-dependency constraint unchanged. The new API surface (owned({ handle }), owned({ refresh: true }), username_required error code) is exactly what this push exercises. No cross-repo API surface exposed.


Summary

Two substantive commits land cleanly: claim-code-alone UX is correct, the second-address claim flow is well-guarded, and primary address selection is correctly persisted. One minor follow-up: wire the bind-recovery result in claim() to setReceiver so a silently-recovered address surfaces in the current session without requiring navigation (src/lib/receive/lnurlRail.ts:311). The long-standing gap for status === 'failed' rendering in QrCode.tsx remains open but unchanged.

Kukks added 3 commits October 28, 2026 13:59
Master's #1107 fails when a translation key is not referenced by a literal
t('...') call. The claim error messages were reached through a code-to-key
map and a dynamic t(key), so six used keys read as unused. They are now
literal t() calls in a code-to-message map; the messages are unchanged.
After a claim that registered its address but failed to bind it, the
background owned({ refresh: true }) bound the address, but its result was
dropped. A first-time claimer stayed on the error until they left the
screen. A wallet that had no address when it claimed now adopts the one the
refresh finds and clears the error. A wallet that already had one keeps it
(Arkana).
@Kukks

Kukks commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. The new minor finding (the refresh after a failed claim repaired the bind, but its result was dropped) is fixed in 16f8314e. A wallet that had no address when it claimed now adopts the address the refresh finds and clears the error. A wallet that already had one keeps it, since the refresh returns the primary or session address rather than the new claim. useLnurlRail.test.ts › "binds an address its claim registered but failed to bind" now also checks the address is adopted and the error is cleared.

@Kukks
Kukks merged commit 3d6e71f into master Oct 28, 2026
8 checks passed

@arkana-ai-bot arkana-ai-bot 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.

Follow-up pass on 6a3a874 — 5 commits since 1011ecf; all prior actionable items remain fixed except one still-open nit; no new blocking findings.


Prior-issue closure

1. server?.domain optional access in yours TableData — Fixed (confirmed in earlier pass, unchanged)

2. async handleChange without await — Fixed (confirmed in earlier pass, unchanged)

3. payRequest() rejection not surfaced in UI — Fixed (confirmed in earlier pass, unchanged)

4. Merge-order dependency on #1083 (receiveViaLnurl/receiveViaTokens) — Fixed (split/p11 landed in base, confirmed in prior pass)

5. No test for lnurlRail.status === 'failed' render path in the receive screen — Still open
Nothing in this increment addresses it. Still low risk for a display-only code path; carrying forward as a follow-up recommendation.


New findings (incremental diff only)

TransactionsList.tsx — .content class coupling (src/components/TransactionsList.tsx, lines 276–278)

const getScrollElement = () => parentRef.current?.closest<HTMLElement>('.content') ?? null

@tanstack/react-virtual degrades gracefully when getScrollElement returns null (it falls back to rendering everything, effectively disabling virtualisation). If the class name content is ever renamed or the component is embedded in a scroll root that lacks that class, virtualisation silently stops working with no error or warning — the regression would only appear as a performance problem, not a test failure. The new virtualisation test verifies the row count under the happy-path layout, but it would still pass with virtualisation disabled (all 300 rows rendered) because the expect(rendered).toBeLessThan(30) assertion would fire. Consider asserting a lower bound (e.g. > 10) alongside the upper bound to make the test a two-sided gate, and consider adding a data-testid or a dev-mode invariant log when the scroll parent cannot be located. Nit, not a blocker.

lnurlRail.ts — namedElsewhere silent-fail (src/lib/receive/lnurlRail.ts, lines 271–282)

facade.addresses()
  .then((all) => !stale && setNamedElsewhere(all.some((a) => a.status === 'active' && a.lightningAddress)))
  .catch((err) => consoleError(err, 'lnurl address list failed'))

If facade.addresses() rejects, namedElsewhere stays false and the "Pick one for me" option appears to a wallet that already holds a named address. This is an UX regression (unnecessary option shown), not a security or protocol issue. The stale-flag cleanup is correct. Acceptable as-is; noting for completeness.

lnurlRail.ts — claimErrorMessages(t) semantics shift (src/lib/receive/lnurlRail.ts, lines 141–143)

- const key = CLAIM_ERROR_KEYS[error.code as LnurlAddressErrorCode]
- if (key) return t(key)
+ const message = claimErrorMessages(t)[error.code as LnurlAddressErrorCode]
+ if (message) return message

The guard changed from "does the key exist?" to "is the translated string truthy?". An empty translation (blank string for a given locale) would fall through to the generic fallback. In practice every locale entry is non-empty, so this is harmless — but if a future translator sets a key to "" as a placeholder, the fallback message appears instead, silently. The motivation (making literal t('...') calls visible to the i18n linter's regex) is sound and the approach is correct; just noting the minor semantic drift.

i18n.ts / i18n.parity.test.ts — unused-keys lint (src/test/lib/i18n.parity.test.ts, lines 25–58)

The collectUsedKeys scan is regex-based on source text. Keys composed at runtime (e.g. `section.${variable}`) won't be found. Two manual exception blocks already exist (the lightningReceive/Send stem loop and the SETTINGS_OPTION_KEY injection). This is a known limitation of static-text analysis and the test is a net positive; no action needed. The cleanup of ~50 unused keys is clean and parity between en and es is maintained.


Summary

No protocol-critical paths touched. No new blocking issues. The five commits are clean: the virtualiser fix correctly targets the outer scroll container and computes scrollMargin via useLayoutEffect; the ownsName/namedElsewhere fix correctly suppresses the random-name option for wallets that hold an address elsewhere; the i18n cleanup removes genuinely dead keys and the new two-sided lint test locks future state. The one carry-over gap (test for the failed rail render path) remains a recommended follow-up.

Kukks added a commit that referenced this pull request Oct 29, 2026
The unreachable-server test asserted only the swap fallback; it now also asserts the 'Lightning address unavailable' line the failed rail renders, as on #1084.
Kukks added a commit that referenced this pull request Oct 29, 2026
…e base screen does

The redesign defaulted every method to a payment link: lightning:<invoice>,
bitcoin:?ark=, bitcoin:<address>. The base screen (#1084) puts the bare value
in the QR for Lightning (the invoice, else the Lightning address, else the
LNURL), Arkade and Bitcoin, and that is what payers' wallets scan. Each
method's default now matches it. The two exceptions are deliberate: Unified
and stablecoins keep their link, and Arkade and Bitcoin keep the link once
an amount is set, where the bare address would silently drop the amount.
A part the user picks is still remembered over the default.

The Copy button beside the amount comes back. It copies what the QR shows,
or a token quote's bare deposit address, since a wallet's address field
rejects the payment URI. The raw-value caveat under the QR goes. A bare
value is now the default, and the base screen never warned about it.
Kukks added a commit that referenced this pull request Nov 5, 2026
The unreachable-server test asserted only the swap fallback; it now also asserts the 'Lightning address unavailable' line the failed rail renders, as on #1084.
Kukks added a commit that referenced this pull request Nov 5, 2026
…e base screen does

The redesign defaulted every method to a payment link: lightning:<invoice>,
bitcoin:?ark=, bitcoin:<address>. The base screen (#1084) puts the bare value
in the QR for Lightning (the invoice, else the Lightning address, else the
LNURL), Arkade and Bitcoin, and that is what payers' wallets scan. Each
method's default now matches it. The two exceptions are deliberate: Unified
and stablecoins keep their link, and Arkade and Bitcoin keep the link once
an amount is set, where the bare address would silently drop the amount.
A part the user picks is still remembered over the default.

The Copy button beside the amount comes back. It copies what the QR shows,
or a token quote's bare deposit address, since a wallet's address field
rejects the payment URI. The raw-value caveat under the QR goes. A bare
value is now the default, and the base screen never warned about it.
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