Skip to content

chore: release v2.1.15#61

Merged
Kukks merged 16 commits into
masterfrom
chore/v2.1.15
May 21, 2026
Merged

chore: release v2.1.15#61
Kukks merged 16 commits into
masterfrom
chore/v2.1.15

Conversation

@Kukks

@Kukks Kukks commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Release v2.1.15

Bumps BTCPayServer.Plugins.ArkPayServer to 2.1.15 and refreshes CHANGELOG.md for everything that's landed on master since v2.1.14, plus the NNark SDK pickup pinned at master tip fa6092d (which now includes #96).

Patch bump matches the project's house convention (2.1.x has been bumped patch even for feature-y entries like 2.1.5's new SDK API).

What's in the release

Plugin (#45#64):

SDK pickup (NNark 171d11afa6092d):

Internal-only PRs (docs cleanup #56 / #57, CI/test wiring #54 / #60) intentionally omitted from the user-facing changelog.

Test plan

  • Plugin build clean (0 errors) after rebase + submodule pin at NNark master tip fa6092d.
  • CHANGELOG entry matches the existing prose style.
  • csproj <Version> = 2.1.15.
  • CI green on the bumped commit.
  • Once merged: tag v2.1.15 and push to trigger the GitHub Release workflow.

@arkanaai arkanaai 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 Code Review — chore: release v2.1.15

Verdict: APPROVE

Scope

Two files changed, both non-functional:

  1. BTCPayServer.Plugins.ArkPayServer.csproj:10 — version bump 2.1.142.1.15. Clean single-line change.
  2. CHANGELOG.md:3-29 — new [2.1.15] entry inserted at top.

Changelog Accuracy

Cross-referenced every entry against merged PRs #45#59:

Changelog entry PR
Per-wallet diagnostic log download #46
Boltz referral id #49
BIP-21 boarding address in Receive #58
Max fee deduction #47
RPC blip crash fix #48
Third-party BIP-21 params on checkout #52
Setup wizard close button #45
New X buttons demoted to text #59

Internal PRs (#54 test wiring, #56/#57 docs) correctly omitted from user-facing changelog.

SDK section references NNark issues (#39, #77#80, #83–#85, #90, #92) picked up via submodule bumps in #50 and #53. Format matches existing bold-lead + prose style.

Protocol-Critical Items (FYI)

The changelog documents two protocol-critical NNark SDK additions:

  • Unilateral exit support (#39) — non-cooperative VTXO → UTXO path
  • Recovery: reconcile stranded Submit→Finalize transactions (#90)

These were already merged to master via #50/#53 and reviewed at that time. This PR only documents them — no new protocol code here.

No Issues Found

  • No code changes to audit for correctness, security, or performance
  • Version follows house convention (patch bump for 2.1.x)
  • Date 2026-05-20 matches today

Ship it. 🚢

Plugin: receive page UX (Link tab default + boarding-embedded BIP-21
+ quieter 'New X' buttons), Send Max-fee deduction, RPC-blip
resilience during fee estimation, checkout preserves third-party
BIP-21 params, initial-setup close button, diagnostic log download,
Boltz referral tagging.

SDK pickup (NNark 171d11a -> a89d47a): unilateral exit, HD gap-limit
recovery, per-wallet sync cursor, multi-provider swap architecture,
pending-tx reconciliation, WalletId-scoped logging, single persistent
Boltz websocket, swap-stuck 404 timeout, chain-time fallback,
EF Core SQLite DateTimeOffset fix.
…anual"

The boarding vs receive flows were writing two different Source metadata
values ("manual-boarding" vs "manual") even though the ContractType
("Boarding" vs "Payment") already discriminates them. Consolidate to a
single "manual" origin tag and let the type filter do the work.

- ArkController.Receive: both call sites now write Source = "manual"
- FindManualReceiveAddress: filter contractTypes: [ArkPaymentContract.ContractType]
  so it can't cross-match a boarding entry
- FindManualBoardingAddress + Contracts view badge: accept legacy
  "manual-boarding" alongside "manual" so existing wallets keep
  rendering their manual address without a data migration

@arkanaai arkanaai 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 Code Review — Incremental (new commit 2e10538)

Verdict: APPROVE

What changed since last review

Commit 2e10538 (refactor(receive): unify manual-boarding/manual contract Source to "manual") — the only new code change on top of the previously-reviewed release commit.

Analysis

The refactoring is correct and backward-compatible.

Before: boarding contracts tagged Source = "manual-boarding", receive contracts tagged Source = "manual". The Source string was the sole discriminator between the two in FindManualReceiveAddress.

After: both write Source = "manual". Discrimination now relies on contractTypes (the proper structural field), not metadata:

Method contractTypes filter Source match Status
FindManualReceiveAddress ArkPaymentContract.ContractType (added by this PR) == "manual" ✓ Won't cross-match boarding
FindManualBoardingAddress ArkBoardingContract.ContractType (already existed) is "manual" or "manual-boarding" (backward compat added) ✓ Finds both old and new
Contracts.cshtml badge N/A is "manual" or "manual-boarding" (backward compat added) ✓ Old contracts still show "Manual" badge

Key correctness check: after this change, a new boarding contract tagged Source = "manual" cannot leak into FindManualReceiveAddress because the contractTypes: [ArkPaymentContract.ContractType] filter excludes it at the storage layer. Confirmed FindManualBoardingAddress already had its own contractTypes filter on master (ArkController.cs:540).

Cross-repo impact

Source metadata is plugin-internal UI tagging — no downstream SDK or API consumers. Grep of all ["Source"] writes confirms "manual-boarding" was only set in ArkController.cs:481 (the line this PR changes). No migration needed.

Not protocol-critical

This touches contract metadata display/lookup, not VTXO handling, signing, forfeit paths, or exit logic. No protocol-level review required.

Clean refactoring. Ship it. 🚢

…min to 5000 sats

The hard-coded 330 was doing double duty as both the P2TR dust floor
(real protocol limit) and the seeded default for new stores. Split into
two named constants on ArkadePaymentMethodConfig:

- DefaultMinBoardingAmountSats = 5000L  (default for new stores; raised
  from 330 — 330 was a confusing default since it was identical to the
  floor and gave operators no headroom)
- P2trDustLimitSats = 330L              (absolute floor; rejection
  threshold on save)

All callers (ArkController save-boarding command, StoreOverview form
min attribute + help text + sub-dust tooltip) now reference the
constants. Existing stores keep their persisted MinBoardingAmountSats
— only stores created after this commit get the 5000-sat default.

@arkanaai arkanaai 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 Code Review — Incremental (new commit ce61077)

Verdict: APPROVE

What changed since last review

Commit ce61077 (refactor(boarding): centralise dust/default constants, raise default min to 5000 sats) — extracts hardcoded 330 into named constants and raises the new-store default.

Analysis

Correct and backward-compatible.

Two constants on ArkadePaymentMethodConfig:

  • P2trDustLimitSats = 330L — absolute floor (matches the real P2TR witness-v1 dust limit). Used as the validation threshold in save-boarding and the form min attribute.
  • DefaultMinBoardingAmountSats = 5000L — seeded default for new stores. Only hits via the record's default parameter; existing stores keep their persisted value.

Both are const → valid as record default parameter values (compile-time constant).

Caller Before After
ArkadePaymentMethodConfig:8 record default 330 DefaultMinBoardingAmountSats (5000)
ArkController.cs:2013 fallback for MinBoardingAmountSats <= 0 330L DefaultMinBoardingAmountSats
ArkController.cs:2014 floor check < 330 < P2trDustLimitSats
ArkController.cs:2015 error message hardcoded string interpolated from P2trDustLimitSats
StoreOverview.cshtml:132 sub-dust tooltip ~330 sats interpolated from P2trDustLimitSats
StoreOverview.cshtml:448 form min 330 P2trDustLimitSats
StoreOverview.cshtml:450 help text 330 sats interpolated from P2trDustLimitSats
README.md boarding row 330 sats 5000 sats + floor note

Not protocol-critical

This is a UI/config default change. No VTXO handling, signing, forfeit, round, or exit logic touched. The P2TR dust limit of 330 sats is correct per BIP 341 + Bitcoin Core's GetDustThreshold for witness v1 outputs.

@arkanaai arkanaai 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 Code Review — Incremental (new commit ce61077)

Verdict: APPROVE

What changed since last review

Commit ce61077 (refactor(boarding): centralise dust/default constants, raise default min to 5000 sats) — extracts hardcoded 330 into named constants and raises the new-store default.

Analysis

Correct and backward-compatible.

Two constants on ArkadePaymentMethodConfig:

  • P2trDustLimitSats = 330L — absolute floor (matches the real P2TR witness-v1 dust limit). Used as the validation threshold in save-boarding and the form min attribute.
  • DefaultMinBoardingAmountSats = 5000L — seeded default for new stores. Only hits via the record's default parameter; existing stores keep their persisted value.

Both are const → valid as record default parameter values (compile-time constant).

Caller Before After
ArkadePaymentMethodConfig:8 record default 330 DefaultMinBoardingAmountSats (5000)
ArkController.cs:2013 fallback for MinBoardingAmountSats <= 0 330L DefaultMinBoardingAmountSats
ArkController.cs:2014 floor check < 330 < P2trDustLimitSats
ArkController.cs:2015 error message hardcoded string interpolated from P2trDustLimitSats
StoreOverview.cshtml:132 sub-dust tooltip ~330 sats interpolated from P2trDustLimitSats
StoreOverview.cshtml:448 form min 330 P2trDustLimitSats
StoreOverview.cshtml:450 help text 330 sats interpolated from P2trDustLimitSats
README.md boarding row 330 sats 5000 sats + floor note

Not protocol-critical

This is a UI/config default change. No VTXO handling, signing, forfeit, round, or exit logic touched. The P2TR dust limit of 330 sats is correct per BIP 341 + Bitcoin Core's GetDustThreshold for witness v1 outputs.

ArkadeCheckoutModelExtension is registered as IGlobalCheckoutModelExtension
(to hide LN/LNURL tabs when Arkade is shown) AND its constructor took
IEnumerable<IGlobalCheckoutModelExtension> (to replay other plugins'
bitcoin-tab contributions onto the Arkade tab). The enumerable forces DI
to build every implementation before our ctor returns — including
ourselves — producing a cycle. Microsoft.Extensions.DependencyInjection's
StackGuard masks the recursion by re-running each level on a fresh thread
blocked on WaitOne, so the request never wrote a single byte (no log
entry, TTFB never observed). dotnet-stack on the live process showed six
threads parked on <RegisterPluginServices>b__11_3 → GetRequiredService
→ StackGuard.RunOnEmptyStackCore.

Switched to IServiceProvider + lazy GetServices<IGlobalCheckoutModelExtension>()
at enumeration time. The singleton is cached by the time we look, so the
cycle never forms.

@arkanaai arkanaai 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 Code Review — Incremental (new commit c04a9f0)

Verdict: APPROVE

What changed since last review

Single new commit: c04a9f0 (fix(checkout): break DI cycle that hung /stores/{id}/invoices).

Analysis

The fix is correct.

The class ArkadeCheckoutModelExtension is itself registered as IGlobalCheckoutModelExtension (ArkPlugin.cs:267) and previously injected IEnumerable<IGlobalCheckoutModelExtension> in its constructor. DI must resolve every implementation to build the enumerable — including this class — creating a cycle. .NET's StackGuard masked the recursion by spawning threads that blocked on WaitOne, hanging the request silently.

The fix replaces the eager IEnumerable<> injection with IServiceProvider, then lazily calls _serviceProvider.GetServices<IGlobalCheckoutModelExtension>() at enumeration time (ArkadeCheckoutModelExtension.cs:130).

Verified:

  • Singleton safety: ArkPlugin.cs:265-267 registers the type as singleton with forwarding factories — GetServices at request time always hits the cached instance. The comment's "singleton is cached by the time we look" claim holds.
  • Thread safety: GetServices on a built IServiceProvider is thread-safe.
  • Self-recursion guard: ReferenceEquals(global, this) at line 132 already existed — still in place.
  • Performance: GetServices on a built container with cached singletons is a dictionary lookup — negligible overhead vs the previous eager injection.
  • CHANGELOG: Accurate description of the bug mechanism (StackGuard thread starvation) and the fix.

Not protocol-critical

Checkout model / UI rendering pipeline. No VTXO, signing, forfeit, or exit paths touched.

@arkanaai arkanaai 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 Code Review — Incremental (new commit c04a9f0)

Verdict: APPROVE

What changed since last review

Single new commit: c04a9f0 (fix(checkout): break DI cycle that hung /stores/{id}/invoices).

Analysis

The fix is correct.

The class ArkadeCheckoutModelExtension is itself registered as IGlobalCheckoutModelExtension (ArkPlugin.cs:267) and previously injected IEnumerable<IGlobalCheckoutModelExtension> in its constructor. DI must resolve every implementation to build the enumerable — including this class — creating a cycle. .NET's StackGuard masked the recursion by spawning threads that blocked on WaitOne, hanging the request silently.

The fix replaces the eager IEnumerable<> injection with IServiceProvider, then lazily calls _serviceProvider.GetServices<IGlobalCheckoutModelExtension>() at enumeration time (ArkadeCheckoutModelExtension.cs:130).

Verified:

  • Singleton safety: ArkPlugin.cs:265-267 registers the type as singleton with forwarding factories — GetServices at request time always hits the cached instance. The comment's "singleton is cached by the time we look" claim holds.
  • Thread safety: GetServices on a built IServiceProvider is thread-safe.
  • Self-recursion guard: ReferenceEquals(global, this) at line 132 already existed — still in place.
  • Performance: GetServices on a built container with cached singletons is a dictionary lookup — negligible overhead vs the previous eager injection.
  • CHANGELOG: Accurate description of the bug mechanism (StackGuard thread starvation) and the fix.

Not protocol-critical

Checkout model / UI rendering pipeline. No VTXO, signing, forfeit, or exit paths touched.

Nigiri 0.5.14's `ark` subcommand hardcodes the container name `ark`, but
arkd v0.9 split the wallet into a sidecar and renamed the daemon container
to `arkd` (signer is now `ark-wallet`). Every cheat-mode call — send,
receive, settle — therefore failed at the docker daemon with
`No such container: ark exit status 1`.

Replaced the `nigiri ark …` calls with `docker exec <ark-container> ark …`
(container name overridable via `ARKADE_CHEAT_ARK_CONTAINER`, defaulting
to `arkd`). Pulled the process-spawn boilerplate out of `Execute` into a
shared `RunProcess(fileName, args, display)` so the nigiri path (rpc /
faucet) and the ark-container path share a single implementation.

Side effect: the "not enough funds" recovery path previously fed
`$(nigiri ark receive | jq -r ...)` to `Process.Start`, which never
expands because we don't spawn a shell — that line silently no-op'd.
Replaced it with a proper two-step call (JSON-parse the receive output
in C#, hand the address to `nigiri faucet`).

@arkanaai arkanaai 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 Code Review — Incremental (new commit 28138e7)

Verdict: APPROVE

What changed since last review

Single new commit: 28138e7 (fix(cheat-mode): exec ark CLI against the renamed arkd container).

Analysis

The fix is correct and the refactoring is a clear improvement.

The core change: nigiri ark <args>docker exec arkd ark <args>, because nigiri 0.5.14's ark subcommand hardcodes docker exec ark … but the arkd v0.9 wallet split renamed the container to arkd.

Area Assessment
ExecuteArk routing Correct — builds docker exec {ArkContainer} ark {subcommand}, WSL-wraps on Windows. Parallel structure with Execute (nigiri commands) is clean.
ArkContainer env override Good escape hatch. Reads env on every call (property, not field) — fine for cheat-mode frequency.
Faucet recovery path Material improvement. Old code $(nigiri ark receive | jq -r ...) never actually expanded under Process.Start (no shell interpretation). New code: ExecuteArk("receive") → parse JSON in C# → pass address to Execute($"faucet {onchainAddr} 2"). Correct and now actually works.
RunProcess extraction Clean refactor from the old GetProcessInfo + inline Process block.
CHANGELOG Accurate description of both the container rename fix and the shell-substitution side-fix.

Pre-existing nits (not blocking, not introduced by this commit)

  1. Process not disposedProcess implements IDisposable; a using or await using would be hygienic. Low-risk in cheat mode's call frequency.
  2. Sequential stdout/stderr readsawait ReadToEndAsync() on stdout then stderr can theoretically deadlock if the child fills the stderr pipe buffer while blocked on stdout. Task.WhenAll on both reads is the robust pattern. Again, cheat-mode output is small so unlikely to hit in practice.
  3. Unbounded recursion — both catch blocks call return await PayInvoice(payInvoiceContext) with no retry cap. If faucet/settle doesn't resolve the condition, this stack-overflows. Pre-existing.

Not protocol-critical

Cheat mode is a regtest-only dev tool. No VTXO handling, signing, forfeit, round, or exit paths touched.

@arkanaai arkanaai 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 Code Review — Incremental (commits ce61077..99635ea)

Verdict: APPROVE

Four new commits since last review

1. ce61077 — Centralise dust/default constants, raise default to 5000 sats

Clean refactor. Two concerns checked:

  • Self-referencing record default (long MinBoardingAmountSats = ArkadePaymentMethodConfig.DefaultMinBoardingAmountSats) — valid C#; const resolves at compile time, no runtime circularity.
  • Existing stores unaffected — persisted values stay as-is. Only new stores get the 5000-sat default. The 330-sat floor (P2trDustLimitSats) is still enforced server-side in the save-boarding handler. ✓
  • All three consumer sites (controller, view input min, view help text) now reference the constant. No stale literals remain.

2. c04a9f0 — Break DI cycle that hung /stores/{id}/invoices

Good fix for a real production hang. The class is registered as IGlobalCheckoutModelExtension AND was injecting IEnumerable<IGlobalCheckoutModelExtension> — classic DI cycle. IServiceProvider + lazy GetServices<>() at enumeration time is the standard workaround. The ReferenceEquals(global, this) recursion guard is still present as a second safety net. Comment explaining the "why" is thorough.

Minor note: IServiceProvider injection is technically the service-locator pattern, but here it's justified and well-documented. No action needed.

3. 28138e7 — Fix cheat-mode for arkd v0.9 container rename

Dev-only code (ICheckoutCheatModeExtension), no production impact. Review notes:

  • Execute vs ExecuteArk split is correct. nigiri rpc/nigiri faucet talk to bitcoind (no container rename issue). Only ark CLI calls need docker exec arkd. ✓
  • Broken shell substitution fixed. The old $(nigiri ark receive | jq -r ...) never worked inside Process.Start (no shell interpretation). Now properly two-step: parse JSON in C#, pass address to faucet. ✓
  • No command injection risk. Process.Start with Arguments doesn't invoke a shell, so metacharacters in destination/onchainAddr can't escape. ✓
  • ArkContainer env var override — good for CI/custom topologies.
  • Pre-existing: both catch blocks recursively call PayInvoice without a depth limit. Unlikely to loop (settle resolves the condition), but a maxRetries guard would be defensive. Not blocking — this is dev-only cheat mode and the recursion predates this PR.

4. 99635ea — Bump NNark submodule + changelog

Submodule pointer fa6092dc8d1484. Changelog entry for #97 (contract type+script in import/derive logs) is accurate. No code to review.

Cross-repo impact

None. ArkadePaymentMethodConfig constants are plugin-internal. No public API, proto, or interface changes in this batch.

Not protocol-critical

None of these commits touch VTXO handling, transaction signing, forfeit paths, round lifecycle, or exit logic. The DI fix and constants refactor are infrastructure; cheat-mode is dev-only.

…r settle

`ark receive` returns two distinct P2TR addresses:
- boarding_address — the boarding script arkd will accept as a settle input
- onchain_address — a plain BTC receive address; funds land in onchain
  balance but are NOT settle-able

The "not enough funds" recovery path was funding onchain_address, so
arkd never saw a settle-able input and aborted with
"fees (0) exceed total amount (0)". Switched the JSON key to
boarding_address and added a 6-block mine between faucet and settle to
clear arkd's validateBoardingInput confirmation check.

@arkanaai arkanaai 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 Code Review — Incremental (new commit 17cf8fd)

Verdict: APPROVE

What changed since last review

Single new commit: 17cf8fd (fix(cheat-mode): faucet the boarding_address (not onchain_address) for settle).

Analysis

The fix is correct.

Two changes in the not enough funds recovery path (ArkadeCheckoutCheatModeExtension.cs:91-108):

Change Why
onchain_addressboarding_address ark settle requires a P2TR boarding script UTXO as input. Fauceting onchain_address deposits to a plain BTC address — arkd's wallet sees it as regular onchain balance but settle silently ignores it, failing with "fees (0) exceed total amount (0)". boarding_address yields a UTXO in the boarding script that validateBoardingInput accepts.
Added Execute("rpc --generate 6") after faucet validateBoardingInput requires a confirmed boarding UTXO. Without mining, the unconfirmed input triggers the same "fees exceed total" abort. 6 blocks matches the regtest helper convention.

Correctness checks:

  • JObject.Parse(receiveJson).GetValue("boarding_address") — matches ark receive JSON schema. Null-guard throws descriptive exception. ✓
  • Execute($"faucet {boardingAddr} 2")boardingAddr is a P2TR address from ark CLI, no shell metacharacters possible via Process.Start. ✓
  • Execute("rpc --generate 6") — uses Execute (nigiri path), not ExecuteArk (docker-exec path). Correct — rpc talks to bitcoind via nigiri, not the arkd container. ✓
  • Sequence: faucet → mine → settle → retry. Correct ordering. ✓
  • Comment block (lines 92-98, lines 100-104) accurately explains the boarding vs onchain distinction and the confirmation requirement. ✓

Pre-existing (not blocking, not introduced by this commit)

Unbounded recursion on both catch branches (return await PayInvoice(payInvoiceContext)) — flagged in prior review, still present. Low-risk for dev-only cheat mode.

Not protocol-critical

Cheat mode is regtest-only. No VTXO handling, signing, forfeit, round, or exit logic touched.

Kukks added 4 commits May 21, 2026 12:36
…just Payment

ArkadePaymentMethodHandler.ConfigurePrompt tags both the Payment contract
(the offchain Arkade address) and the Boarding contract (when boarding
is enabled) with Source = "invoice:{id}". The invoice listener's
ToggleArkadeContract resolved a single contract via the prompt's
GetArkAddress() — i.e. the Payment one — so the Boarding entry's
ActivityState was never flipped to Inactive on settlement. The boarding
contract kept being polled, kept showing up under "active contracts,"
and blocked the script subscription from unsubscribing.

Now finds every contract whose metadata Source matches "invoice:{id}"
and toggles them all together.

HTLC contracts created by the Lightning swap path use a different
"swap:{id}" Source tag and remain driven by OnSwapChanged based on
swap state, not invoice state — out of scope here.
In the Contracts and ListWallets tables, the count cells branched between
a `badge`-styled span (count > 0) and a plain `text-muted` span (count == 0).
Badges carry their own padding / line-height, so rows where one column
had a badge and the other didn't visibly nudged out of alignment. Switched
the zero case to `<span class="badge text-bg-light text-muted fw-normal">0</span>` —
same box dimensions as the populated case, but visually muted.
When a contract row had no VTXOs or swaps to reveal, the chevron button
was omitted entirely. Because the actions cell is text-end, the remaining
refresh + delete buttons collapsed rightward, so the refresh icon sat in
two different horizontal positions across rows — visibly misaligned.

Always render the chevron button; mark it invisible / disabled /
aria-hidden when there's nothing to expand. The icon column positions
are now identical across all rows.
…able

The "Arkade Payments" section on invoice details mis-rendered any payment
that arrived via the boarding address as if it were an off-chain VTXO
payment:

- Address column showed the prompt's Ark `tark1…` address instead of the
  boarding `bcrt1p…` / `bc1p…` address.
- Contract column showed the off-chain Payment contract string instead of
  the Boarding contract.
- Outpoint link routed through the Arkade explorer even though the
  outpoint was a real BTC chain txid.

ArkadePaymentData now carries an explicit `IsBoarding` flag (set by
ArkContractInvoiceListener.OnVtxoChanged when it matched a Boarding
contract). ArkadePromptDetails now carries `BoardingContractString`
alongside the existing `BoardingAddress`. The view renders a
"Boarding" / "VTXO" badge per row and routes the address + outpoint
links through BTCPay's `TransactionLinkProviders["BTC-CHAIN"]` when the
row is on-chain, falling back to the Arkade explorer for VTXO rows.

For old payment rows that pre-date the `IsBoarding` flag, a fallback
compares the recorded destination against the prompt's BoardingAddress
to detect boarding rows without requiring a data migration.

@arkanaai arkanaai 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 Code Review — Incremental (commits 17cf8fd..2ac4c59)

Verdict: APPROVE

New commits (4)

Commit Summary
eb43be7 fix(invoice): deactivate ALL invoice-tagged contracts on settle
6a28996 fix(views): align zero-count cells with badge cells
060ed13 fix(views): reserve expand-row slot in Contracts actions column
2ac4c59 fix(invoice): render boarding payments correctly in Arkade Payments table

eb43be7 — ToggleArkadeContract rewrite ⚠️ (contract lifecycle)

The fix is correct. Old code derived a single script from GetArkAddress() and toggled only that contract — the Payment one. Boarding contracts tagged Source = "invoice:{id}" were never deactivated, leaking as permanently Active (polled forever, cluttering the contracts view, preventing script unsubscription).

New code: fetch all contracts for the wallet, filter in-memory by Metadata["Source"] == "invoice:{id}", toggle each. HTLC contracts use swap:{id} prefix so they're correctly excluded. Verified against OnSwapChanged (line ~63) which independently manages swap contract lifecycle by script — no collision.

Two minor notes (non-blocking):

  1. ArkContractInvoiceListener.cs:243GetContracts(walletIds: [walletId]) fetches all contracts for the wallet with no isActive/contractTypes filter. For a store with hundreds of historical contracts this is heavier than necessary. Could pass isActive: true when activityState == Inactive (i.e. when deactivating on settle — you only need to deactivate currently-active ones). Not a blocker for a release branch, but worth a follow-up.

  2. ArkContractInvoiceListener.cs:248 — The sequential UpdateContractActivityState calls inside the foreach loop issue one DB round-trip per matching contract (typically 2: payment + boarding). Fine for the expected cardinality but if you ever batch more aggressively, a UpdateContractActivityStates(walletId, scripts[], state) batch method would be cleaner.


2ac4c59 — Boarding payment rendering

Clean implementation. Three things verified:

  • IsBoarding flag propagation: OnVtxoChanged queries contractStorage.GetContracts(contractTypes: [ArkBoardingContract.ContractType]) to detect boarding → passes isBoarding to HandlePaymentData → persisted on ArkadePaymentData record. Correct.
  • Backward-compat fallback (ArkPaymentData.cshtml:31-33): Old payment rows lacking the IsBoarding flag fall back to destination-equality check against promptDetails.BoardingAddress. Good — avoids a data migration.
  • BoardingContractString added to ArkadePromptDetails and populated in ArkadePaymentMethodHandler.cs:96-99. The boarding row now shows the correct contract, not the payment contract.

One minor nit (ArkPaymentData.cshtml:80): When TransactionLinkProviders.GetTransactionLink(btcChainPmi, txid) returns null (no BTC-CHAIN provider configured), addrLink falls back to string.Empty. If truncate-center renders this as href="", that's a self-link on click. Consider falling back to "#" or omitting the link param entirely. Non-blocking.


6a28996 + 060ed13 — View alignment fixes

Pure CSS/layout. Zero-count cells now use <span class="badge text-bg-light ...">0</span> to match badge-width cells above. Invisible placeholder button in the actions column prevents delete-icon jitter across rows. Both trivial and correct.


Protocol-critical assessment

eb43be7 touches contract lifecycle management (activation/deactivation of boarding contracts tied to invoices). This is adjacent to protocol-critical territory — incorrect deactivation could cause a contract to stop being monitored, potentially missing a boarding payment. However:

  • The fix adds deactivation coverage (boarding contracts were never deactivated before — they were leaked as active), so the failure mode was over-monitoring, not under-monitoring.
  • The source-tag filter is deterministic and invoice-ID-scoped — no risk of accidentally toggling unrelated contracts.
  • The isBoarding flag on ArkadePaymentData is display-only; it does not affect payment settlement logic.

No protocol-critical flag needed for this batch.

Ship it. 🚢

…silently noop)

ArkContractInvoiceListener.HandlePaymentData was patching the payment
blob via JObject.Parse + `blob["Destination"] = destination` after the
.Set(...) extension initialised it from the prompt. The default BTCPay
JSON serialiser uses camelCase, so the on-disk property name is
`destination`. JObject is case-sensitive — the patch wrote a sibling
`Destination` key the deserialiser ignored, so the prompt's Ark address
survived as the top-level Blob2.destination and `payment.Destination`
in the UI always returned the off-chain Ark address even when the
funds had actually arrived via boarding. This is the root cause of the
"Arkade Payments table shows VTXO for boarding payment" rendering bug.

Switched to the lowercase `destination` key and remove any stale
capital-D sibling left behind by earlier runs of this code.

Also extended the Arkade Payments view's fallback to consult
`paymentData.Destination` (always set correctly from ArkadePaymentData)
before `payment.Destination`, so existing payment rows with the
wrong top-level destination still render as Boarding.

Verified on store 4K6LVRZ…/invoice VGVirSb…: the listener correctly
imported both Payment + Boarding contracts (matching Source tag), the
boarding VTXO landed in storage with Confirmed=True, but the payment
row's top-level destination ended up Ark while details.destination
correctly stored the bcrt1p boarding address — exactly the pattern
this fix addresses.

@arkanaai arkanaai 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 Code Review — Incremental (new commit d28f097)

Verdict: APPROVE

What changed since last review

Single new commit: d28f097 (fix(payment): boarding destination override used capital-D JSON key (silently noop)).

Analysis

The fix is correct and well-reasoned.

Two files changed (plus a changelog line):

1. ArkContractInvoiceListener.cs:178-195 — lowercase JSON key fix

Before After
blob["Destination"] = destination blob["destination"] = destination + blob.Remove("Destination")

BTCPay's PaymentData.Set() serialises via Newtonsoft's camelCase contract resolver → on-disk key is "destination". JObject indexer is case-sensitive, so blob["Destination"] was silently creating a second sibling key that the deserialiser never read. The prompt's Ark address survived as payment.Destination even for boarding payments.

Fix writes the correct lowercase key and defensively removes any stale capital-D sibling from rows persisted by earlier code. Both operations are safe — JObject.Remove on a missing key is a no-op.

The comment block (lines 180-186) is accurate and saves the next developer from the same trap. ✓

2. ArkPaymentData.cshtml:29-40 — view fallback chain

var paymentDestination = paymentData.Destination ?? payment.Destination;
var isBoarding = paymentData.IsBoarding
    || (!string.IsNullOrEmpty(promptDetails.BoardingAddress)
        && string.Equals(paymentDestination, promptDetails.BoardingAddress, ...));

Before: the isBoarding fallback compared payment.Destination (the broken top-level blob field) against the boarding address — always false for old rows since the bug stored the Ark address there. Now: prefers paymentData.Destination (from ArkadePaymentData.details, which was always written correctly) before falling back to the top-level. This correctly heals old rows without a data migration. ✓

The address and contractString derivations also use paymentDestination consistently. ✓

Edge cases verified

  • New boarding payments (post-fix): IsBoarding = true in ArkadePaymentData, lowercase destination correct in blob. All three detection paths agree. ✓
  • Old boarding payments (pre-fix, capital-D bug): IsBoarding = false (flag didn't exist), payment.Destination = wrong Ark address, but paymentData.Destination = correct boarding address. Fallback chain: paymentData.Destination ?? payment.Destination → correct boarding address → matches promptDetails.BoardingAddressisBoarding = true. ✓
  • VTXO payments (non-boarding): IsBoarding = false, paymentData.Destination = Ark address ≠ BoardingAddressisBoarding = false. ✓
  • Very old payments (no Destination in details at all): paymentData.Destination = null → falls back to payment.Destination → Ark address ≠ boarding address → isBoarding = false → renders as VTXO. Correct — these predated boarding support. ✓

Not protocol-critical

This touches payment display rendering and blob metadata, not VTXO handling, transaction signing, forfeit paths, round lifecycle, or exit logic.

Ship it. 🚢

…anged events

Adds InvoicePaymentLatencyTests.CheatModePay_DirectArkTx_InvoiceSettlesWithinThreshold
to NArk.E2E.Tests. The test:

- Creates a merchant store + Arkade wallet via the existing helper
- Once-per-suite, initialises the in-arkd `ark` CLI and funds it via
  a single boarding+settle (kept out of the timed window so the
  measurement isolates the plugin's invoice-settle latency rather than
  arkd's batch round)
- Creates a 5000-sat ARKADE invoice and triggers BTCPay's cheat-mode
  POST /i/{id}/test-payment route, which routes through the plugin's
  ArkadeCheckoutCheatModeExtension and `docker exec arkd ark send`
  (an out-of-round Ark tx — essentially instant on arkd's side)
- Polls GetInvoice() at 100ms resolution until status == Settled
- Two assertion modes:
  - HardTimeout (30s) → Assert.Fail with a "missed-event regression"
    message if the invoice never settles (catches a broken
    VtxosChanged → OnVtxoChanged → AddPayment chain)
  - LatencyThreshold (12s) → Assert.True against the elapsed time when
    it does settle (catches a slowness regression — currently observed
    real-world is ~6s)

Threshold deliberately wide to absorb CI jitter today; tighten as the
in-handler gap between UpsertVtxo and invoice_paymentSettled is
localised and fixed.

@arkanaai arkanaai 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 Code Review — Incremental (new commit c50e740)

Verdict: APPROVE

What changed since last review

Single new file: NArk.E2E.Tests/InvoicePaymentLatencyTests.cs (295 lines) — an E2E test that times cheat-mode invoice payment latency and guards against missed VtxosChanged events.

Analysis

Well-structured regression test with clear dual purpose:

  1. Latency regression — asserts invoice settles within 12s (2× the observed ~6s baseline). Tight 100ms polling loop gives sub-second resolution. ✓
  2. Missed-event regression — 30s hard timeout catches the case where VtxosChanged never fires at all, failing as a hard error rather than a CI flake. ✓

Correctness checks:

Area Status
EnsureArkdCliReadyAsync double-checked locking via SemaphoreSlim + _arkdCliReady flag ✓ Classic async DCL pattern, correct
Setup sequence: ark initark receivenigiri faucet → mine 6 → ark settle ✓ Matches the boarding→settle flow. 6-block confirm is consistent with cheat-mode fix in 17cf8fd
Balance threshold check (10,000 sats) after settle ✓ Defensive — catches silent settle failures
GetArkdOffchainSatsAsync JSON parsing with try/catch JsonException ✓ Handles uninitialised CLI gracefully
Invoice created with Amount = 5000m, Currency = "SATS" ✓ Matches cheat-mode extension's routing
Cheat pay via Playwright APIRequest.PostAsync to /i/{id}/test-payment ✓ Correct BTCPay cheat-mode endpoint
InvoiceStatus.Invalid early-exit with descriptive assertion ✓ Separates "payment rejected" from "payment slow"
Timer starts at t0 before POST, stops on Settled poll ✓ Measures full end-to-end including network

Minor observations (non-blocking):

  1. InvoicePaymentLatencyTests.cs:62_arkdCliSetupLock and _arkdCliReady are static, so they survive across test class instantiations within the same process. This is intentional (fund once per suite), and there's only one [Fact] in this class today. If more facts are added later, the static state is correct — the semaphore serialises and the bool gates. Fine as-is.

  2. InvoicePaymentLatencyTests.cs:128 — The polling loop measures DateTimeOffset.UtcNow - t0 at the moment Settled is observed, which includes the last poll interval (~100ms jitter). For a 12s threshold this is negligible but worth knowing if the threshold is ever tightened below 1s.

  3. InvoicePaymentLatencyTests.cs:198container resolution happens inside the lock, so the docker exec calls are serialised. If a future test class also calls EnsureArkdCliReadyAsync, the SemaphoreSlim(1,1) correctly prevents concurrent arkd setup races.

Not protocol-critical

Pure test infrastructure. No VTXO handling, transaction signing, forfeit paths, round lifecycle, or exit logic modified.

Ship it. 🚢

@arkanaai arkanaai 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 Code Review — Incremental (new commit c50e740)

Verdict: APPROVE

What changed since last review

Single new file: NArk.E2E.Tests/InvoicePaymentLatencyTests.cs (295 lines) — an E2E test that times cheat-mode invoice payment latency and guards against missed VtxosChanged events.

Analysis

Well-structured regression test with clear dual purpose:

  1. Latency regression — asserts invoice settles within 12s (2× the observed ~6s baseline). Tight 100ms polling loop gives sub-second resolution. ✓
  2. Missed-event regression — 30s hard timeout catches the case where VtxosChanged never fires at all, failing as a hard error rather than a CI flake. ✓

Correctness checks:

Area Status
EnsureArkdCliReadyAsync double-checked locking via SemaphoreSlim + _arkdCliReady flag ✓ Classic async DCL pattern, correct
Setup sequence: ark initark receivenigiri faucet → mine 6 → ark settle ✓ Matches the boarding→settle flow. 6-block confirm is consistent with cheat-mode fix in 17cf8fd
Balance threshold check (10,000 sats) after settle ✓ Defensive — catches silent settle failures
GetArkdOffchainSatsAsync JSON parsing with try/catch JsonException ✓ Handles uninitialised CLI gracefully
Invoice created with Amount = 5000m, Currency = "SATS" ✓ Matches cheat-mode extension's routing
Cheat pay via Playwright APIRequest.PostAsync to /i/{id}/test-payment ✓ Correct BTCPay cheat-mode endpoint
InvoiceStatus.Invalid early-exit with descriptive assertion ✓ Separates "payment rejected" from "payment slow"
Timer starts at t0 before POST, stops on Settled poll ✓ Measures full end-to-end including network

Minor observations (non-blocking):

  1. InvoicePaymentLatencyTests.cs:62_arkdCliSetupLock and _arkdCliReady are static, so they survive across test class instantiations within the same process. This is intentional (fund once per suite), and there's only one [Fact] in this class today. If more facts are added later, the static state is correct — the semaphore serialises and the bool gates. Fine as-is.

  2. InvoicePaymentLatencyTests.cs:128 — The polling loop measures DateTimeOffset.UtcNow - t0 at the moment Settled is observed, which includes the last poll interval (~100ms jitter). For a 12s threshold this is negligible but worth knowing if the threshold is ever tightened below 1s.

  3. InvoicePaymentLatencyTests.cs:198container resolution happens inside the lock, so the docker exec calls are serialised. If a future test class also calls EnsureArkdCliReadyAsync, the SemaphoreSlim(1,1) correctly prevents concurrent arkd setup races.

Not protocol-critical

Pure test infrastructure. No VTXO handling, transaction signing, forfeit paths, round lifecycle, or exit logic modified.

Ship it. 🚢

…ers + antiforgery

Three issues found running the test against the live nigiri stack:

1. ASP.NET MVC controllers default to FORM binding for complex parameters
   without [FromBody]; `UIInvoiceController.TestPayment(FakePaymentRequest)`
   silently defaulted PaymentMethodId="BTC" when the body was JSON, and
   GetCheatModeExtension(... "BTC") returned null on an Arkade-only store
   → 400 "No ICheatModeExtension registered". Switched to
   application/x-www-form-urlencoded.

2. BTCPay enforces antiforgery on every UI-controller POST via the
   UIControllerAntiforgeryTokenAttribute global filter (registered in
   Startup); missing token = 400 with an empty body. Grab the token off
   the Arkade overview page and pass it in the RequestVerificationToken
   header.

3. (Minor) Document the matching binding rules in comments so the next
   person hitting an empty-body 400 on this endpoint can find the cause.

Local run on the nigiri stack: test passes with measured latency ~2s,
well under the 12s threshold. Plugin debug log decomposes as:
  arkd push  T=0
  UpsertVtxo +1.1s (750ms intentional delay)
  invoice_paymentSettled +1.3s (185ms from upsert)

@arkanaai arkanaai 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 Code Review — Incremental (new commit 6a8297d)

Verdict: APPROVE

What changed since last review

Single commit 6a8297d (test(e2e): fix InvoicePaymentLatencyTests POST to match BTCPay's binders + antiforgery) — test-only, no production code touched.

Analysis

Two fixes to InvoicePaymentLatencyTests.cs, both correct:

  1. Content-Type switched from JSON to application/x-www-form-urlencoded (InvoicePaymentLatencyTests.cs:116-121). BTCPay's UIInvoiceController.TestPayment binds from form data (no [FromBody]), so the previous JSON POST silently fell back to defaults (PaymentMethodId="BTC") and the Arkade cheat extension never matched. The form-encoded payload Amount=5000&CryptoCode=SATS&PaymentMethodId=ARKADE matches the MVC binder correctly.

  2. Antiforgery token added (InvoicePaymentLatencyTests.cs:95-100). Navigates to the Arkade overview to seed a RequestVerificationToken, then passes it in the header. Matches the pattern used in SpendingTests, ApiEndpointTests, SwapsTests, and FundedWalletTests — all call the same GetAntiforgeryTokenAsync() from PlaywrightBaseTest.

Not protocol-critical

Test infrastructure only — no VTXO, signing, forfeit, or exit code changed.

…r claim)

Picks up two SDK perf merges since #97 landed:

- #99 perf(sync): single immediate poll per stream push, drop
  750ms/3s/8s retry schedule — perceived VTXO arrival latency drops to
  one network round-trip on a healthy connection, no more "still
  nothing" log churn.
- #100 perf(wallet): cache OutputDescriptor.Parse + BIP-39 master
  ExtKey — per-receive claim ~800x faster; biggest impact on HD
  wallets where the KDF dominated cold claim cost.

CHANGELOG v2.1.15 SDK NNark section updated accordingly. No plugin
code changes required — both are internal perf wins that downstream
consumers get for free. Build clean against the new pointer.

@arkanaai arkanaai 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 Code Review — Incremental (new commit aed2290)

Verdict: APPROVE

What changed since last review

Single commit aed2290 (chore: bump NNark to b55c886 (perf: sync responsiveness + ~800x faster claim)) — submodule bump + two new CHANGELOG entries. No btcpay-arkade production code changed.

NNark submodule analysis (c8d1484..b55c886 — 2 commits)

1. b55c886 — perf(sync): single immediate poll per stream push, drop 750ms/3s/8s retry schedule (#99)

VtxoSynchronizationService.cs: removes the FirePollSchedule method and its StreamPushPollSchedule delays (750ms/3s/8s). Replaced with a single immediate _readyToPoll.Writer.WriteAsync on stream push. The RoutinePoll at 5s interval (VtxoSynchronizationService.cs:213) acts as the safety net if arkd's indexer lags the event.

✅ Correct simplification. The retry schedule was defensive against arkd v0.9.0-rc.1 indexer lag; on v0.9.5+ it's dead weight. Worst case: immediate poll misses → routine 5s poll catches it. No VTXO loss vector.

2. a6aa8a3 — perf(wallet): cache OutputDescriptor.Parse and BIP-39 master extKey (#100)

Three caches introduced:

Cache Location Key Value Thread-safe Bounded
_descriptorCache KeyExtensions.cs (descriptor_string, network_id) OutputDescriptor ConcurrentDictionary By unique descriptors (small) ✓
_extKeyCache HierarchicalDeterministicWalletSigner.cs mnemonic string ExtKey (master private key) ConcurrentDictionary By unique wallets (tiny) ✓
Removed wasted parse HierarchicalDeterministicAddressProvider.cs:27 N/A N/A N/A N/A

Security check on _extKeyCache: The mnemonic string is already held in wallet.Secret (in-memory ArkWalletInfo). The ExtKey (master private key) was already derived and used transiently on every call. Promoting it to a static cache doesn't expand the attack surface — the key material lifetime is already process-scoped. The cache just avoids re-running PBKDF2-HMAC-SHA512 × 2048 on every Sign/SignMusig/GetPubKey/GenerateNonces. ✓

Logic-preservation check on VHTLCContractTransformer.cs: The CanTransform refactor unwraps the short-circuit && into nested if blocks for instrumentation. Logic is identical:

  • Before: if (preimage != null && IsOurs(Receiver)) → return GetSigner() != null
  • After: if (preimage != null) { if (IsOurs(Receiver)) { return GetSigner() != null } }

IsOurs fix (HierarchicalDeterministicAddressProvider.cs:27): The original OutputDescriptor.Parse(wallet.AccountDescriptor, network) was a pure side-effect-free call whose return value was discarded — a ~500ms no-op on every IsOurs invocation. Replaced with a string.IsNullOrEmpty null-guard that preserves the validation intent. ✓

Diagnostic probes (non-blocking observation)

~15 TEMP latency probe Stopwatch + LogTrace blocks added across CoinService, SpendingService, SweeperService, DefaultWalletProvider, VHTLCContractTransformer. All gated behind LogTrace (off by default). Comments say "TEMP" — should be cleaned up in a follow-up once the bottleneck analysis is complete. Fine for now.

CHANGELOG accuracy

Two new entries under "SDK (NNark)":

  • VTXO sync responsiveness → matches #99 ✓
  • Per-receive claim ~800× faster → matches #100 ✓

Not protocol-critical

Performance optimizations only. No changes to VTXO handling, transaction construction, forfeit paths, round lifecycle, or exit logic. The signing flow is unchanged — only the KDF and parse hot paths are cached.

Ship it. 🚢

…container

The plugin's ArkadeCheckoutCheatModeExtension defaults to docker
exec'ing into 'arkd' — the SDK's documented v0.9-split topology
(arkd-override compose: container_name: ${ARK_CONTAINER:-arkd}).
nigiri's base compose, however, ships the daemon as 'ark' and that
service name wins in CI; start-env.sh's auto-derivation in
.env.defaults didn't promote the override to 'arkd'. Result:
CheatModePay_DirectArkTx_InvoiceSettlesWithinThreshold has been
failing on the branch since ~11:30 UTC today with 'No such container:
arkd'. The plugin exposes ARKADE_CHEAT_ARK_CONTAINER for exactly this
case; pinning to 'ark' matches what nigiri actually creates.

Pre-existing CI failure, not caused by the submodule bump in
aed2290 — same test failed identically on 26225732848 (last run
before my push).

@arkanaai arkanaai 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 Code Review — Incremental (13 new commits since 2e10538)

Verdict: APPROVE

Scope of new changes

Commit Area Risk
ce61077 Centralise dust/default constants, raise default boarding min to 5000 Low
c04a9f0 Break DI cycle that hung /stores/{id}/invoices Medium — DI lifecycle
28138e7 Cheat-mode: exec ark CLI against renamed arkd container Low (test infra)
99635ea Bump NNark to c8d1484 (contract type+script in import/derive logs) Low
17cf8fd Cheat-mode: faucet boarding_address (not onchain_address) Low (test infra)
eb43be7 Deactivate ALL invoice-tagged contracts on settle High — contract lifecycle
6a28996 Views: align zero-count cells with badge cells Cosmetic
060ed13 Views: reserve expand-row slot in Contracts actions column Cosmetic
2ac4c59 Render boarding payments correctly in Arkade Payments table Medium — view correctness
d28f097 Fix boarding destination override: Destinationdestination JSON key High — payment data integrity
c50e740 E2E: latency test for cheat-mode pay Low (test)
6a8297d E2E: fix POST binding to match BTCPay's MVC binder Low (test)
aed2290 Bump NNark to b55c886 (perf: sync + 800× faster claim) Low
b79ecc2 CI: set ARKADE_CHEAT_ARK_CONTAINER=ark for nigiri Low (CI)

Protocol-adjacent findings (reviewed, all correct)

1. ToggleArkadeContract now deactivates ALL invoice-tagged contracts (eb43be7)

Old code only toggled the Payment contract (derived from prompt's GetArkAddress()); the Boarding contract stayed Active forever after settlement — leaking a subscription and polluting the active-contracts list.

New code queries all contracts for the wallet, filters in-memory by Source == "invoice:{id}", and toggles each. Functionally correct: ConfigurePrompt tags both Payment and Boarding contracts with the same Source = "invoice:{id}" metadata, and HTLC contracts use a different "swap:{id}" tag so they're excluded. The activityState is derived from invoice.Status == New ? Active : Inactive — correct state machine.

Minor perf note (non-blocking): GetContracts(walletIds: [walletId]) fetches the entire contract set for the wallet with no isActive/contractTypes filter because we need to find contracts in any state (activate vs deactivate depends on invoice state). IContractStorage.GetContracts doesn't support metadata filtering, so the in-memory Where is the only option today. For stores with thousands of contracts this is O(n) per invoice state change. Fine for now; if it shows up in profiles, a metadata filter parameter on GetContracts would be the right fix in NNark.

2. HandlePaymentData destination JSON key fix (d28f097)

Set() serialises ArkadePaymentData via BTCPay's camelCase JsonSerializer, producing "destination" (lowercase). The old code patched blob["Destination"] (capital-D), which — because JObject is case-sensitive — added a shadow key the deserialiser ignored. The boarding address was silently lost on read. Fix is correct: write blob["destination"], remove stale "Destination". Defensive and backward-compatible.

3. IsBoarding flag propagation (2ac4c59, d28f097)

ArkadePaymentData gains IsBoarding, ArkadePromptDetails gains BoardingContractString. Both flow through OnVtxoChanged → HandlePaymentData → ArkadePaymentData → Blob2 and are consumed in ArkPaymentData.cshtml to render the correct badge ("Boarding" vs "VTXO") and route outpoint links to TransactionLinkProviders["BTC-CHAIN"] for on-chain payments. The view has a two-level fallback for old rows lacking the flag: paymentData.Destination → prompt's BoardingAddress comparison. Correct.

4. DI cycle fix (c04a9f0)

ArkadeCheckoutModelExtension is both an ICheckoutModelExtension and an IGlobalCheckoutModelExtension. Injecting IEnumerable<IGlobalCheckoutModelExtension> into its constructor caused the DI container to resolve all implementations (including itself) before the constructor returned — a cycle that StackGuard papered over by spinning up threads, deadlocking /stores/{id}/invoices. Switching to IServiceProvider + lazy GetServices<>() at enumeration time breaks the cycle cleanly. The ReferenceEquals(global, this) guard is retained. Correct.

5. Cheat-mode container rename (28138e7, 17cf8fd)

nigiri ark ...docker exec {ArkContainer} ark ... with ArkContainer defaulting to "arkd" (overridable via ARKADE_CHEAT_ARK_CONTAINER). No shell injection risk: Process.Start doesn't invoke a shell. The recovery path's broken shell substitution ($(nigiri ark receive | jq -r ...) — never expanded under Process.Start) is now a proper two-step call: parse JSON in C#, pass the address into nigiri faucet. Also correctly faucets boarding_address instead of onchain_address, and mines 6 blocks before settle (required: validateBoardingInput needs confirmed UTXOs). All test infra, not production.

6. Boarding minimum default raised (ce61077)

DefaultMinBoardingAmountSats = 5000L, P2trDustLimitSats = 330L. Constants centralised, all callers reference them. Existing stores keep their persisted value; only new stores get 5000. README updated. Clean.

E2E test review (InvoicePaymentLatencyTests.cs)

Solid regression test. Double-check locking via SemaphoreSlim + _arkdCliReady for one-time arkd CLI setup. Form-urlencoded POST matches BTCPay's MVC binder (not JSON — the old JSON path silently fell back to PaymentMethodId="BTC"). 12s threshold with 2× headroom over observed 6s is reasonable for CI jitter. Hard 30s timeout catches missed-event regressions. No concerns.

Cross-repo impact

  • Source metadata is plugin-internal — no downstream consumers.
  • ArkadePaymentData.IsBoarding and ArkadePromptDetails.BoardingContractString are new fields with defaults (false, null) — backward-compatible with existing serialised data.
  • NNark submodule bump is a fast-forward; the plugin already consumed the new APIs in prior commits.
  • No public API surface changes that would affect ts-sdk, go-sdk, or other consumers.

Not protocol-critical

None of these changes touch VTXO construction, transaction signing, forfeit paths, round lifecycle, connector trees, or exit paths. The contract lifecycle changes (activate/deactivate) are metadata-level bookkeeping — they control which contracts the plugin polls, not how VTXOs are created or redeemed. No human sign-off required beyond normal review.

Ship it. 🚢

@Kukks Kukks merged commit 7b2f9a3 into master May 21, 2026
2 checks passed
@Kukks Kukks deleted the chore/v2.1.15 branch May 21, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant