Adds a second downstream lane to /apis/btcmaps/v1/submit that forwards
the merchant payload to teambtcmap/btcmap-api's submit_place RPC
(merged 2026-05-24 in teambtcmap/btcmap-api#91).
Request schema:
- New fields Lat, Lon, Category, ExternalId on BtcMapsSubmitRequest,
required iff SubmitToBtcMap=true. Validator enforces lat/lon
ranges, lowercase-identifier category, and 1-200 char external_id.
Plugin side (rollforsats/BTCPayServerPlugins) composes external_id
as hostname:storeId so the namespace stays unique per BTCPay
instance.
- New SubmitToDirectory + SubmitToBtcMap routing flags. The
directory flag defaults true to preserve existing callers; btcmap
defaults false so new callers must opt in.
- New Phone field forwarded as OSM Key:phone in extra_fields.
Service layer:
- BtcMapsService.SubmitToBtcMapAsync POSTs a JSON-RPC 2.0 envelope
({jsonrpc, method, params, id}) to BTCMAPS:BtcMapImportEndpoint
(default https://api.btcmap.org/rpc) with method=submit_place,
origin=btcpayserver, and the merchant payload mapped to the
documented param shape. Bearer auth from BTCMAPS:BtcMapImportToken.
- Optional fields (website, description, twitter, github, onion,
phone, country) ride along in extra_fields using OSM tag keys
(contact:twitter, addr:country, etc.) plus the implicit
payment:bitcoin=yes marker.
- New BtcMapTokenMissingException parallels the existing
DirectoryTokenMissingException so the controller can return 503
with a distinct error code when ops haven't provisioned the
scoped token yet.
Controller:
- /apis/btcmaps/v1/submit branches on SubmitToDirectory +
SubmitToBtcMap. At least one must be true (rejected 400 otherwise).
- Each lane has its own exception ladder symmetric to the existing
directory path: token-missing 503 (directory-not-configured /
btcmap-not-configured), caller-cancel rethrow, upstream-timeout
504, generic-failure 502 - error codes namespaced by lane so ops
can tell them apart.
HttpClient registration:
- New HttpClientNames.BtcMap named client registered with 15s
per-call timeout and JSON Accept header, matching the
BtcMapsDirectory budget for bounded worst-case behavior.
Tests:
- 12 new validation tests in BtcMapsServiceTests covering the
SubmitToBtcMap=true required-field paths (Lat / Lon / Category /
ExternalId; range checks; lowercase-identifier policy; overlong
external_id) plus the default-false directory-only-still-works
baseline. 37/37 BtcMapsServiceTests passing.
Addresses thgO-O review id 4274558448 (PR #224) items #2 and #3.
#2 OperationCanceledException vs HttpClient.Timeout: the prior
catch-when filter excluded OCE wholesale to avoid swallowing client
disconnects, but HttpClient.Timeout surfaces as TaskCanceledException
(inherits from OCE) so an upstream GitHub timeout slipped through and
fell out as a 500 instead of the intended 502. Now split into three
arms: caller-cancel rethrows (info-log + drop the connection),
upstream-timeout returns 504 with discriminant directory-upstream-
timeout, anything else still 502 directory-upstream-failed.
#3 type/subType case normalization on write: ValidTypes +
ValidMerchantSubTypes match OrdinalIgnoreCase at validation, but the
write at BuildMerchantEntry preserved the user-submitted casing. A
submission of "Merchants" / "Books" would pass validation and land
non-canonical in merchants.json. Normalize to ToLowerInvariant on
write so the file stays in the lowercase convention. Country is
already validated against a case-sensitive Ordinal set so the
validator rejects non-uppercase before reaching the writer; no write-
path change needed there.
Addresses post-#224 review feedback from @rollforsats + CodeRabbit:
- IHttpClientFactory + named HttpClientNames.BtcMapsDirectory client
replaces per-request `new HttpClient()`. 15s per-call timeout caps the
~5-7 GitHub round-trips at a bounded worst case instead of the default
100s x N. Bearer token stays per-request (the BTCMAPS token is distinct
from the global PluginBuilder GitHub token; must not leak into the
singleton handler).
- Markdown injection guard on the PR body. User fields (Name, Type,
SubType, Country, Twitter, GitHub) are wrapped in inline code spans
with backtick-escape so a doctored merchant name can't render as a
clickable link in the maintainer-facing PR description. Description
goes inside a fenced code block. URL is rendered as <bare-url> autolink
so the maintainer always sees the actual destination.
- Idempotent branch name: SHA-1-derived suffix from the normalized URL
replaces the random GUID. Two concurrent same-URL submissions now
collide on `git/refs` create instead of racing through preflight and
opening duplicate PRs. The 422 "Reference already exists" surface is
caught and mapped to the open-PR lookup or `branch-exists-no-open-pr`.
- NormalizeUrl lowercases scheme + host only and preserves path + query
case verbatim. Lowercasing the whole URL falsely de-duplicates
case-sensitive paths.
- Country code validation moves to an actual ISO 3166-1 alpha-2 set
built from CultureInfo at startup. Replaces the
`length==2 && IsUpper` shape that accepted reserved/unassigned codes
like ZZ.
- Missing BTCMAPS:DirectoryGithubToken throws
DirectoryTokenMissingException at the service layer; controller maps
it to 503 with `directory-not-configured`. Previously surfaced as a
200 OK with `Skipped` which a client could misread as "accepted".
5 new tests:
- Validate_RejectsNonAssignedTwoLetterCountry (ZZ)
- NormalizeUrl_PreservesPathCase
- NormalizeUrl_PreservesQueryCase
- BuildBranchName_DeterministicForSameUrl
- BuildBranchName_DiffersForDifferentUrls
25/25 BtcMapsServiceTests pass on Release build.
Supersedes PR #211. Per-store OSM OAuth moves to the BTC Map
plugin side (rollforsats/BTCPayServerPlugins PR #5); the
plugin-builder side keeps only the directory PR submission.
Drops vs PR #211:
- TagOnOsmAsync / UnlistFromOsmAsync service paths and all
OSM XML + changeset infrastructure (~430 lines)
- TagOnOsm / UnlistFromOsm / OsmNodeId / OsmNodeType /
Latitude / Longitude / OsmCategory / AcceptsLightning
request fields
- BtcMapsOsmResult response shape + Address sub-model
- OSM-specific validators
Keeps:
- POST /apis/btcmaps/v1/submit opens a PR against
btcpayserver/directory.btcpayserver.org's merchants.json
- GET /apis/btcmaps/v1/ping
- Rate limit: 5 submissions / 24h per source IP
- Validation for name / url / description / type / subType /
country / twitter / github / onionUrl
Build clean (Release); 20 unit tests cover validation, slug,
URL normalization.
Add an explicit local artifact download proxy flag for development and tests.
Keep the public download endpoint as a redirect while routing enabled loopback artifacts through the internal proxy.