Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
b3dc622
feat(lnurlverify): request the LUD-XX lightning payment option within…
Kukks Oct 1, 2026
6cb2191
feat(lnurlverify): track and persist each invoice's verifyBatch endpoint
Kukks Oct 1, 2026
4c8bcfd
feat(lnurlverify): answer GetInvoice from the poller's last verify re…
Kukks Oct 1, 2026
1cc0b3b
feat(lnurlverify): one-shot LUD-XX verifyBatch client
Kukks Oct 1, 2026
3e8cccd
feat(lnurlverify): poll verifyBatch endpoints with one request per cycle
Kukks Oct 1, 2026
c47e21a
test(lnurlverify): mint signed regtest BOLT11s and pin the payment-ha…
Kukks Oct 1, 2026
39b8228
test(lnurlverify): settle through a real lnurl-server's verifyBatch
Kukks Oct 1, 2026
782d905
release(lnurlverify): v1.1.0 - verifyBatch polling and LUD-XX lightni…
Kukks Oct 1, 2026
6ff1ccd
fix(lnurlverify): fall back to per-invoice polling when a verifyBatch…
Kukks Oct 1, 2026
998f5ca
feat(lnurlverify): rail table and BIP321 URI merging for paymentOptio…
Kukks Oct 1, 2026
92139bf
feat(lnurlverify): request a paymentOptions rail destination and refu…
Kukks Oct 1, 2026
9c52827
fix(lnurlverify): refuse a rail whose verify URL is not http(s)
Kukks Oct 1, 2026
17566ac
feat(lnurlverify): poll rail destinations alongside invoices through …
Kukks Oct 1, 2026
a24c9e8
feat(lnurlverify): decide a store's rails from what its LNURL advertises
Kukks Oct 1, 2026
9b4cb39
feat(lnurlverify): LNURL rail payment methods, inactive until a check…
Kukks Oct 1, 2026
f6334b2
test(lnurlverify): pin the rail handler's activation and re-issue paths
Kukks Oct 1, 2026
bbb12c7
feat(lnurlverify): record settled rail destinations and re-issue them…
Kukks Oct 1, 2026
7194fac
fix(lnurlverify): keep rail settlements retrying and rebuilds per-inv…
Kukks Oct 1, 2026
e66a7e1
feat(lnurlverify): provision LNURL rails from the store's advertised …
Kukks Oct 1, 2026
b1273c8
fix(lnurlverify): reconcile rails on a fresh store copy and only from…
Kukks Oct 1, 2026
e3de76d
feat(lnurlverify): anonymous rail activation for the checkout, rememb…
Kukks Oct 1, 2026
f3876bd
fix(lnurlverify): single-flight rail activations and rate-limit them …
Kukks Oct 1, 2026
82a62c6
feat(lnurlverify): one Bitcoin checkout tab with a merged BIP321 QR a…
Kukks Oct 1, 2026
4b13964
fix(lnurlverify): keep the checkout QR live, hide stale rails, keep N…
Kukks Oct 2, 2026
3b6759e
fix(lnurlverify): claim a Lightning fallback only when the Lightning …
Kukks Oct 2, 2026
d239a42
feat(lnurlverify): store setting for requesting every rail when the c…
Kukks Oct 2, 2026
2e946d7
fix(lnurlverify): bind the settings store from the route, confirm sav…
Kukks Oct 2, 2026
9956fc9
fix(lnurlverify): bind the activation invoice from the route only
Kukks Oct 2, 2026
0e19dc0
test(lnurlverify): hold an Arkade-enabled lnurl-server address open f…
Kukks Oct 2, 2026
cfef3f0
test(lnurlverify): open the runbook session with its own token so the…
Kukks Oct 2, 2026
68fba1d
release(lnurlverify): v1.2.0 - one BIP321 checkout over the LNURL's p…
Kukks Oct 2, 2026
e44cd7b
fix(lnurlverify): switch LNURL rails off on their own page; correct t…
Kukks Oct 2, 2026
4f452e6
fix(lnurlverify): rail switches touch only the rails the page showed,…
Kukks Oct 2, 2026
574487d
fix(lnurlverify): reach the rails when Lightning fails, refuse an htt…
Kukks Oct 2, 2026
716e2ba
perf(lnurlverify): sweep stores eight at a time so unreachable LNURLs…
Kukks Oct 2, 2026
61fbcd2
fix(lnurlverify): keep the setup tab's inline code inline
Kukks Oct 2, 2026
d38bf42
fix(lnurlverify): read signet invoices so mutinynet Lightning payment…
Kukks Oct 2, 2026
cf92c5b
docs(lnurlverify): verification count is now 188 tests
Kukks Oct 2, 2026
88b734f
fix(lnurlverify): an "All" chip returns the checkout to the merged QR
Kukks Oct 2, 2026
c126e5a
docs(lnurlverify): mention the "All" chip in the checkout description
Kukks Oct 2, 2026
f05aeeb
feat(lnurlverify): skip rails the LNURL cannot verify, and remember t…
Kukks Oct 2, 2026
b866a99
fix(lnurlverify): request a usable option of a rail's type, not just …
Kukks Oct 6, 2026
cf184ee
fix(lnurlverify): prefer a rail option declared verifiable over one t…
Kukks Oct 6, 2026
f5bb19c
fix(lnurlverify): pick a rail option that accepts the amount before f…
Kukks Oct 6, 2026
65d6f86
docs(lnurlverify): match VERIFICATION's test count to the suite (199)
Kukks Oct 6, 2026
25640d0
fix(lnurlverify): ignore an option bound no msat amount can hold inst…
Kukks Oct 6, 2026
91ded84
fix(lnurlverify): plan Lightning on the first available lightning option
Kukks Oct 6, 2026
f1bb5ac
LNURLVerify: pay an invoice in a token on EVM, Solana or Tron (#157)
Kukks Oct 7, 2026
9b41b56
fix(lnurlverify): stop the settings page's LNURL lookup when the requ…
Kukks Oct 7, 2026
200ef01
LNURLVerify: provider-backed token rails, quoted only when the payer …
Kukks Oct 8, 2026
97f6a52
LNURLVerify: a Lightning address tab on the Lightning setup page (#158)
Kukks Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Plugins/BTCPayServer.Plugins.LNURLVerify/Resources/lnurlverify/wallet.js binary linguist-generated=true
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ jobs:
exit 1
fi

- name: Test the LNURL Verify wallet module
working-directory: Plugins/BTCPayServer.Plugins.LNURLVerify/walletconnect
run: |
npm ci --ignore-scripts
npm test
npm run build
git diff --exit-code -- ../Resources/lnurlverify/wallet.js

resolve-plugins:
name: Resolve active plugins
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ docs/plans
.playwright-mcp
docs/superpowers
.mcp.json
node_modules/
63 changes: 63 additions & 0 deletions BTCPayServer.Plugins.LNURLVerify.Tests/Bip321Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using BTCPayServer.Payments;
using Xunit;

namespace BTCPayServer.Plugins.LNURLVerify.Tests;

public class Bip321Tests
{
static KeyValuePair<string, string> KV(string key, string value) => new(key, value);

static RailState Ln(bool active = true) =>
new(PaymentMethodId.Parse("BTC-LN"), "Lightning", active, "lnbcrt1ln", active ? "lightning:lnbcrt1ln" : null, "lightning");
static RailState Chain() =>
new(PaymentMethodId.Parse("BTC-CHAIN"), "On-chain", true, "bcrt1qaddr", "bitcoin:bcrt1qaddr?amount=0.0002&pj=https://x/pj", null);
static RailState Ark(bool active = true) =>
new(LnurlRails.Arkade.PaymentMethodId, "Arkade", active, "tark1qdest", active ? "bitcoin:?amount=0.0001&ark=tark1qdest" : null, "ark");

[Fact]
public void Build_puts_the_amount_first_and_the_address_in_the_path() =>
Assert.Equal("bitcoin:bcrt1qaddr?amount=0.0001&ark=tark1qdest",
Bip321.Build("bcrt1qaddr", 0.00010000m, new[] { KV("ark", "tark1qdest") }));

[Fact]
public void Build_without_an_address_starts_the_query_directly() =>
Assert.Equal("bitcoin:?amount=1&lightning=lnbcrt1ln", Bip321.Build(null, 1m, new[] { KV("lightning", "lnbcrt1ln") }));

[Fact]
public void Amounts_are_invariant_btc_without_trailing_zeros()
{
Assert.Equal("0.00000001", Bip321.Amount(0.00000001m));
Assert.Equal("21", Bip321.Amount(21.00000000m));
}

[Fact]
public void Extend_keeps_existing_parameters_and_skips_a_key_already_present() =>
Assert.Equal("bitcoin:bcrt1qaddr?amount=0.0001&lightning=lnbcrt1old&ark=tark1qdest",
Bip321.Extend("bitcoin:bcrt1qaddr?amount=0.0001&lightning=lnbcrt1old",
new[] { KV("lightning", "lnbcrt1new"), KV("ark", "tark1qdest") }));

[Fact]
public void Qr_uppercases_only_a_bech32_address()
{
Assert.Equal("bitcoin:BCRT1QADDR?amount=0.0001&ark=tark1qdest", Bip321.Qr("bitcoin:bcrt1qaddr?amount=0.0001&ark=tark1qdest"));
Assert.Equal("bitcoin:2NBase58?amount=1", Bip321.Qr("bitcoin:2NBase58?amount=1"));
Assert.Equal("bitcoin:?amount=1&ark=tark1qdest", Bip321.Qr("bitcoin:?amount=1&ark=tark1qdest"));
Assert.Equal("lightning:LNBCRT1LN", Bip321.Qr("lightning:lnbcrt1ln"));
}

[Fact]
public void A_lone_active_rail_keeps_its_own_uri() =>
Assert.Equal("lightning:lnbcrt1ln", Bip321.Merge(new[] { Ln(), Ark(active: false) }, 0.0001m));

[Fact]
public void Merge_extends_the_onchain_link_with_lightning_then_ark() =>
Assert.Equal("bitcoin:bcrt1qaddr?amount=0.0002&pj=https://x/pj&lightning=lnbcrt1ln&ark=tark1qdest",
Bip321.Merge(new[] { Ln(), Chain(), Ark() }, 0.0001m));

[Fact]
public void Merge_without_onchain_carries_the_given_amount() =>
Assert.Equal("bitcoin:?amount=0.0001&lightning=lnbcrt1ln&ark=tark1qdest", Bip321.Merge(new[] { Ln(), Ark() }, 0.0001m));

[Fact]
public void Merge_of_nothing_active_is_null() => Assert.Null(Bip321.Merge(new[] { Ln(false), Ark(false) }, 0.0001m));
}
26 changes: 26 additions & 0 deletions BTCPayServer.Plugins.LNURLVerify.Tests/CaipAssetTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Xunit;

namespace BTCPayServer.Plugins.LNURLVerify.Tests;

public class CaipAssetTests
{
[Fact]
public void A_caip19_id_splits_into_its_network_and_token()
{
var asset = CaipAsset.Parse("eip155:42161/erc20:0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9")!;
Assert.Equal(("eip155", "42161", "erc20", "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"),
(asset.Namespace, asset.ChainReference, asset.AssetNamespace, asset.AssetReference));
Assert.Equal("eip155:42161", asset.ChainId);
Assert.Equal("eip155:42161/erc20:0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", asset.ToString());
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("eip155:42161")]
[InlineData("EIP155:42161/erc20:0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9")]
[InlineData("eip155:42161/erc20:0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9\n")]
[InlineData("eip155:42161/erc20:<script>")]
[InlineData("eip155:42161/erc20:0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9/1")]
public void Malformed_ids_are_refused(string? id) => Assert.Null(CaipAsset.Parse(id));
}
51 changes: 51 additions & 0 deletions BTCPayServer.Plugins.LNURLVerify.Tests/ChainDirectoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Xunit;

namespace BTCPayServer.Plugins.LNURLVerify.Tests;

public class ChainDirectoryTests
{
[Theory]
[InlineData("eip155:42161", "Arbitrum One")]
[InlineData("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", "Solana Devnet")]
[InlineData("tron:0xcd8690dc", "Tron Nile")]
[InlineData("eip155:999999", "eip155:999999")]
public void Networks_are_named_locally_with_the_raw_id_as_fallback(string chain, string label) =>
Assert.Equal(label, ChainDirectory.Label(chain));

[Fact]
public void A_known_chain_links_its_transactions()
{
var evm = "0x" + new string('a', 64);
var tron = new string('b', 64);
var solana = new string('5', 88);
Assert.Equal("https://sepolia.arbiscan.io/tx/" + evm, ChainDirectory.TxUrl("eip155:421614", evm));
Assert.Equal("https://nile.tronscan.org/#/transaction/" + tron, ChainDirectory.TxUrl("tron:0xcd8690dc", tron));
Assert.Equal($"https://solscan.io/tx/{solana}?cluster=devnet", ChainDirectory.TxUrl("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", solana));
}

// lnurl-server's FF_ASSETS chains: one it adds first must fail here rather than reach a payer as a raw CAIP-2 id.
[Theory]
[InlineData("eip155:1")]
[InlineData("eip155:42161")]
[InlineData("eip155:8453")]
[InlineData("eip155:10")]
[InlineData("eip155:137")]
[InlineData("eip155:43114")]
[InlineData("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp")]
[InlineData("tron:0x2b6653dc")]
public void Every_fixedfloat_chain_is_named_and_links_its_deposit_transactions(string chain)
{
var depositTx = chain.Split(':')[0] switch { "eip155" => "0x" + new string('a', 64), "solana" => new string('5', 88), _ => new string('b', 64) };
Assert.NotEqual(chain, ChainDirectory.Label(chain));
Assert.NotNull(ChainDirectory.TxUrl(chain, depositTx));
}

[Fact]
public void A_reference_that_is_not_a_transaction_id_gets_no_explorer_link()
{
Assert.Null(ChainDirectory.TxUrl("eip155:999999", "0x" + new string('a', 64)));
Assert.Null(ChainDirectory.TxUrl("eip155:42161", "not-a-hash"));
Assert.Null(ChainDirectory.TxUrl("eip155:42161", new string('a', 64)));
Assert.Null(ChainDirectory.TxUrl("tron:0xcd8690dc", "0x" + new string('b', 64)));
}
}
164 changes: 164 additions & 0 deletions BTCPayServer.Plugins.LNURLVerify.Tests/CheckoutRailsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using System.Net;
using BTCPayServer.Payments;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Xunit;

namespace BTCPayServer.Plugins.LNURLVerify.Tests;

public class CheckoutRailsTests
{
static readonly PaymentMethodId Ln = PaymentMethodId.Parse("BTC-LN"), Chain = PaymentMethodId.Parse("BTC-CHAIN");
static readonly PaymentMethodId Arkade = LnurlRails.Arkade.PaymentMethodId;

static PaymentPrompt Prompt(PaymentMethodId pmi, bool active) => new() { PaymentMethodId = pmi, Inactive = !active, Currency = "BTC" };

static InvoiceEntity Invoice(params (PaymentMethodId Pmi, bool Active)[] prompts)
{
var invoice = new InvoiceEntity { Id = "inv", Currency = "BTC" };
foreach (var (pmi, active) in prompts) invoice.SetPaymentPrompt(pmi, Prompt(pmi, active));
return invoice;
}

static IReadOnlyList<PaymentPrompt> Mixed() => new[] { Prompt(Ln, true), Prompt(Chain, false), Prompt(Arkade, false) };

[Fact]
public void Only_an_invoice_holding_an_lnurl_rail_gets_the_checkout()
{
Assert.False(CheckoutRails.Applies(Invoice((Ln, true), (Chain, true))));
Assert.True(CheckoutRails.Applies(Invoice((Ln, true), (Arkade, false))));
}

[Fact]
public void Rails_run_lightning_onchain_lnurl_and_the_stores_own_wallet_wins_onchain()
{
var invoice = Invoice((Arkade, false), (LnurlRails.OnChain.PaymentMethodId, false), (Chain, true), (Ln, true));
Assert.Equal(new[] { Ln, Chain, Arkade }, CheckoutRails.Rails(invoice).Select(p => p.PaymentMethodId));
}

[Fact]
public void An_unset_setting_activates_every_inactive_rail_on_open()
{
var (activate, skipped) = CheckoutRails.Plan(Mixed(), null, new LnurlRailSettings().ActivateAllRailsOnOpen, _ => false);
Assert.Equal(new[] { Chain, Arkade }, activate);
Assert.Empty(skipped);
}

[Fact]
public void With_the_setting_off_opening_activates_nothing() =>
Assert.Empty(CheckoutRails.Plan(Mixed(), null, false, _ => false).Activate);

[Fact]
public void A_rail_that_failed_recently_is_not_retried_on_open()
{
var (activate, skipped) = CheckoutRails.Plan(Mixed(), null, true, p => p == Arkade);
Assert.Equal(new[] { Chain }, activate);
Assert.Equal(new[] { Arkade }, skipped);
}

[Fact]
public void A_tap_activates_that_rail_even_after_a_failure_but_never_an_active_one()
{
Assert.Equal(new[] { Arkade }, CheckoutRails.Plan(Mixed(), "LNURL-ARKADE", false, _ => true).Activate);
Assert.Empty(CheckoutRails.Plan(Mixed(), "BTC-LN", true, _ => false).Activate);
}

[Fact]
public void A_failure_is_remembered_for_an_hour_per_invoice()
{
var failures = new RailActivationFailures();
var now = DateTimeOffset.UtcNow;
failures.Record("inv", Arkade, now);
Assert.True(failures.Recent("inv", Arkade, now.AddMinutes(59)));
Assert.False(failures.Recent("inv", Arkade, now.AddMinutes(61)));
Assert.False(failures.Recent("other", Arkade, now));
}

[Fact]
public async Task Overlapping_activations_of_one_rail_run_once()
{
var gate = new RailActivationGate();
var release = new TaskCompletionSource<bool>();
var calls = 0;
Task<bool> Activate() { calls++; return release.Task; }

var first = gate.Run("inv", Arkade, Activate);
var second = gate.Run("inv", Arkade, Activate);
release.SetResult(true);

Assert.True(await first);
Assert.True(await second);
Assert.Equal(1, calls);
}

[Fact]
public async Task A_finished_activation_does_not_absorb_the_next()
{
var gate = new RailActivationGate();
var release = new TaskCompletionSource<bool>();
var calls = 0;
Task<bool> Activate() { calls++; return calls == 1 ? release.Task : Task.FromResult(false); }

var first = gate.Run("inv", Arkade, Activate);
var second = gate.Run("inv", Arkade, Activate);
release.SetResult(true);
await Task.WhenAll(first, second);

Assert.False(await gate.Run("inv", Arkade, Activate));
Assert.Equal(2, calls);
}

[Fact]
public async Task Different_rails_and_invoices_activate_independently()
{
var gate = new RailActivationGate();
var release = new TaskCompletionSource<bool>();
var calls = 0;
Task<bool> Activate() { calls++; return release.Task; }

var runs = new[] { gate.Run("inv", Arkade, Activate), gate.Run("inv", Chain, Activate), gate.Run("other", Arkade, Activate) };
Assert.Equal(3, calls);
release.SetResult(true);

Assert.DoesNotContain(false, await Task.WhenAll(runs));
}

// BTCPay's own forwarded-headers setup (Startup.cs): every sender trusted, the last hop applied.
static IPAddress? PayerOf(string peer, string? forwardedFor, string? originalFor)
{
var context = new DefaultHttpContext();
context.Connection.RemoteIpAddress = IPAddress.Parse(peer);
context.Connection.RemotePort = 40000;
if (forwardedFor is not null) context.Request.Headers["X-Forwarded-For"] = forwardedFor;
if (originalFor is not null) context.Request.Headers["X-Original-For"] = originalFor;
var options = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
new ForwardedHeadersMiddleware(_ => Task.CompletedTask, NullLoggerFactory.Instance, Options.Create(options)).ApplyForwarders(context);
return PayerIp.Resolve(context);
}

[Theory]
[InlineData("203.0.113.9", null, null, "203.0.113.9")]
[InlineData("203.0.113.9", null, "10.0.0.1:5", "203.0.113.9")]
[InlineData("127.0.0.1", "203.0.113.7", null, "203.0.113.7")]
[InlineData("172.18.0.5", "198.51.100.1, 203.0.113.7", null, "203.0.113.7")]
[InlineData("fd00::5", "203.0.113.7", null, "203.0.113.7")]
[InlineData("fe80::5", "203.0.113.7", null, "203.0.113.7")]
[InlineData("::ffff:192.168.1.2", "203.0.113.7", null, "203.0.113.7")]
public void A_direct_connection_or_a_local_proxy_names_the_payer(string peer, string? forwardedFor, string? originalFor, string payer) =>
Assert.Equal(IPAddress.Parse(payer), PayerOf(peer, forwardedFor, originalFor));

[Theory]
[InlineData("203.0.113.9", "198.51.100.1", null)]
[InlineData("203.0.113.9", "198.51.100.1", "10.0.0.1:5")]
[InlineData("203.0.113.9", null, "198.51.100.1:5")]
[InlineData("2001:db8::5", "203.0.113.7", null)]
[InlineData("203.0.113.9", null, "not-an-endpoint")]
public void A_public_peer_or_an_unreadable_original_names_no_payer(string peer, string? forwardedFor, string? originalFor) =>
Assert.Null(PayerOf(peer, forwardedFor, originalFor));
}
52 changes: 52 additions & 0 deletions BTCPayServer.Plugins.LNURLVerify.Tests/CheckoutTokensTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Xunit;

namespace BTCPayServer.Plugins.LNURLVerify.Tests;

public class CheckoutTokensTests
{
const long Due = 20_000_000;
static readonly DateTimeOffset Now = DateTimeOffset.FromUnixTimeSeconds(1_790_000_000);

static TokenNetworkState Quoted(string id, long amountMsat = Due, long? expiresAt = 1_790_000_600) =>
new() { OptionId = id, Quote = new TokenQuoteState { AmountMsat = amountMsat, ExpiresAt = expiresAt } };

static IReadOnlyCollection<string> Plan(string? network, params TokenNetworkState[] networks) =>
CheckoutRails.PlanTokens(new LnurlTokenPromptDetails { Networks = networks.ToList() }, network, Due, Now);

[Fact]
public void Opening_the_tab_requests_nothing() =>
Assert.Empty(Plan(null, new TokenNetworkState { OptionId = "ff-usdtarbitrum" }, new TokenNetworkState { OptionId = "ff-usdttrc" }));

[Fact]
public void A_tap_on_a_network_holding_a_live_quote_requests_nothing()
{
Assert.Empty(Plan("ff-usdtarbitrum", Quoted("ff-usdtarbitrum")));
Assert.Empty(Plan("ff-usdtarbitrum", Quoted("ff-usdtarbitrum", expiresAt: null)));
}

[Theory]
[InlineData("unrequested")]
[InlineData("failed")]
[InlineData("expired")]
[InlineData("stale")]
public void A_tap_requests_a_network_whose_quote_is_not_live(string state)
{
var network = state switch
{
"unrequested" => new TokenNetworkState { OptionId = "ff-usdtarbitrum" },
"failed" => new TokenNetworkState { OptionId = "ff-usdtarbitrum", FailedAt = 1 },
"expired" => Quoted("ff-usdtarbitrum", expiresAt: 1_790_000_000),
_ => Quoted("ff-usdtarbitrum", amountMsat: Due + 1000)
};
Assert.Equal(state, network.State(Due, Now));
Assert.Equal(new[] { "ff-usdtarbitrum" }, Plan("ff-usdtarbitrum", network));
}

[Fact]
public void A_tap_on_a_refused_or_unknown_network_requests_nothing()
{
Assert.Empty(Plan("usdt-tron", new TokenNetworkState { OptionId = "usdt-tron", Refused = true }));
Assert.Empty(Plan("usdt-polygon", new TokenNetworkState { OptionId = "usdt-arbitrum" }));
Assert.Empty(CheckoutRails.PlanTokens(null, "usdt-arbitrum", Due, Now));
}
}
Loading
Loading