Merge a6591ec57dfc94dbb6d577b887bc8e125d2ead14 into 2fc6f4747275bdbfec2288ac1a45f7d5bedcd9ac

This commit is contained in:
heyaim 2026-11-07 05:53:53 +00:00 committed by GitHub
commit 1efe643cab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1364 additions and 435 deletions

View File

@ -19,9 +19,11 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Fetch GeoRelays
# Must match RelayDirectory.ASSET_FILE_URL. This job refreshes the
# bundled asset from the app's runtime source.
run: |
wget https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv
mv nostr_relays.csv ./app/src/main/assets/nostr_relays.csv
wget https://raw.githubusercontent.com/permissionlesstech/bitchat/refs/heads/main/relays/online_relays_gps.csv
mv online_relays_gps.csv ./app/src/main/assets/nostr_relays.csv
- name: Check for changes
id: git-check

View File

@ -1,389 +1,442 @@
Relay URL,Latitude,Longitude
relay.agentry.com,42.8864,-78.8784
relay.veganostr.com:443,60.1699,24.9384
relay.nostrfy.org,35.6916,139.768
nostr.debate.report,50.1109,8.68213
relay.nostr.blockhenge.com,39.0438,-77.4874
memlay.v0l.io,53.3498,-6.26031
relay.monostr.com,50.4754,12.3683
nostr.emanuelemiani.it,45.778,8.79414
nostr.hifish.org,47.4009,8.57814
nostr.aphelion.network,55.7623,37.6381
nostr.synalysis.com,50.4754,12.3683
nip85-staging.nosfabrica.com,39.0997,-94.5786
relay.bnos.space,43.6532,-79.3832
nostr.islandarea.net,35.4669,-97.6473
nostr.tac.lol,47.4748,-122.273
nostr.plantroon.com:443,50.1013,8.62643
nostr-pub.wellorder.net,45.5201,-122.99
relay.ru.ac.th,13.7607,100.627
conduit-congee.fly.dev,38.7946,-106.535
nostr-verified.wellorder.net,45.5201,-122.99
nostr.girino.org,43.6532,-79.3832
nostr.stakey.net,52.6485,4.73787
relay.satsapp.me,50.4754,12.3683
nostr.tagomago.me,42.3601,-71.0589
nostrcity-club.fly.dev,38.7946,-106.535
relay.dyne.org,49.0291,8.35705
nostr.liberty.fans,36.8767,-89.5879
relay.agora.social,50.7383,15.0648
relay.lightning.pub:443,39.0438,-77.4874
relay.thewired.app,39.0438,-77.4874
relay.s-w.art,43.6532,-79.3832
relay5.bitransfer.org,43.6532,-79.3832
nostr.computingcache.com,45.5341,-122.956
openrelay.ziomc.com,41.8967,12.4822
relayone.soundhsa.com,39.0997,-94.5786
nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832
relay.nmail.li,50.9871,2.12554
relay.islandbitcoin.com,20.3898,78.0903
hasky.chat,40.7862,-74.0743
relay.mitosystem.net,-23.5558,-46.6396
nostr.oxtr.dev,50.4754,12.3683
staging.yabu.me,35.6096,139.73
nostr.dzo-hadar.ts.net,35.6764,139.65
relayone.soundhsa.com:443,39.0997,-94.5786
nostr.benderhost.org,53.3498,-6.26031
relay.dreamith.to,43.6532,-79.3832
nostr.n7ekb.net,47.4941,-122.294
relay.nostr.com,50.1109,8.68213
relay.layer.systems,43.6532,-79.3832
relay.budabit.club,49.4543,11.0746
relay2.orangesync.tech,40.7128,-74.006
r.senx.io,50.6924,3.20113
relay.artiostr.ch,43.6532,-79.3832
relay.fiatdenier.com,43.7787,-79.5393
relay.btcforplebs.com,43.6532,-79.3832
nostr.islandarea.net:443,35.4669,-97.6473
cs-relay.nostrdev.com,50.4754,12.3683
public.crostr.com:443,43.6532,-79.3832
relay.nostrmap.net:443,60.1699,24.9384
relay.chatbett.de,40.7128,-74.006
relay.shadowbip.com,54.352,18.6466
nostr.snowbla.de,50.4754,12.3683
portal-relay.pareto.space,53.5488,9.98717
relay.mypathtofire.de,42.8864,-78.8784
nostr.spicyz.io,43.6532,-79.3832
basspistol.org,49.0291,8.35696
relay.notoshi.win,13.3622,100.983
nostrride.io,43.6532,-79.3832
nostr.yutakobayashi.com,43.6532,-79.3832
nostria.at,48.3064,14.2861
relay.gulugulu.moe,43.6532,-79.3832
bruh.samt.st,43.6532,-79.3832
nostr.zeroflux.net,50.4754,12.3683
nostr.pbfs.io:443,50.4754,12.3683
dev.relay.stream,43.6532,-79.3832
nostr.21crypto.ch,47.5356,8.73209
nostr-kyomu-haskell.onrender.com,37.7775,-122.397
relay.fountain.fm,43.6532,-79.3832
relay.bowlafterbowl.com,32.9483,-96.7299
dm-test-strfry-generic.samt.st,43.6532,-79.3832
nostr.mad-social.net,50.4754,12.3683
relay.laantungir.net,-19.4692,-42.5315
relay.manneken.brussels,49.4543,11.0746
nostr.hetuniversum.nl,43.6532,-79.3832
relay.lotek-distro.com,43.6532,-79.3832
nr.yay.so,46.2126,6.1154
pymd.kamp.site,38.9072,-77.0369
relay.nostar.org,43.6532,-79.3832
nostr-02.uid.ovh,50.9871,2.12554
relay.bornheimer.app,50.1109,8.68213
nostrcity-club.fly.dev:443,38.7946,-106.535
relay.hivescope.xyz,40.4332,-3.62431
relay.olas.app,60.1699,24.9384
relay.masize.com,43.6532,-79.3832
relay.edufeed.org:443,49.4521,11.0767
relay.colombiap2p.com,43.6532,-79.3832
nostr.carroarmato0.be,51.0355,4.48285
bcast.girino.org,43.6532,-79.3832
relay.ohstr.com,43.6532,-79.3832
schnorr.me:443,43.6532,-79.3832
relay.npubhaus.com,43.6532,-79.3832
relay.nostx.io,43.6532,-79.3832
relay.keykeeper.world,40.7824,-74.0711
nostr.twinkle.lol,51.902,7.6657
relay.trustr.ing,43.6548,-79.3885
offchain.pub:443,39.1585,-94.5728
dm-test-strfry-discovery.samt.st,43.6532,-79.3832
relay.liberbitworld.org,43.6532,-79.3832
dare-sydney-ethical-aspect.trycloudflare.com,43.6532,-79.3832
relay01.lnfi.network,35.6764,139.65
relay.pacsarcade.org,32.7767,-96.797
relay.nostr.dev.br,48.1046,11.6002
relay.gulugulu.moe:443,43.6532,-79.3832
relay.sincensura.org,43.6532,-79.3832
cyberspace.nostr1.com,40.7057,-74.0136
public.crostr.com,43.6532,-79.3832
relay-fra.zombi.cloudrodion.com,48.8566,2.35222
nostr.quali.chat,60.1699,24.9384
nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874
free.relayted.de,50.1109,8.68213
relay.klabo.world,47.674,-122.122
testr.nymble.world,40.8054,-74.0241
relay.czas.plus,50.1109,8.68213
relay.guggero.org,46.5971,9.59652
relay.kizuna-miki.com,35.694,139.754
nostras.app,38.7946,-106.535
nostr.pro.aw,43.6532,-79.3832
nostr.oxtr.dev:443,50.4754,12.3683
relay.crovaxon.de,53.0894,7.32917
relay.carlismo.mx,50.1109,8.68213
relay-us.zombi.cloudrodion.com,40.7862,-74.0743
relay.satmaxt.xyz:443,43.6532,-79.3832
schnorr.me,43.6532,-79.3832
relay.mappingbitcoin.com,43.6532,-79.3832
relay.t6x.host,40.8302,-74.1299
relay.lanavault.space,60.1699,24.9384
nostr.88mph.life,52.1941,-2.21905
nostr.hoppe-relay.it.com,42.8864,-78.8784
relay.hivetalk.org,40.8302,-74.1299
x.kojira.io,43.6532,-79.3832
relay.formstr.app,43.6532,-79.3832
offchain.pub,39.1585,-94.5728
chorus.bonsai.com,39.0438,-77.4874
relay.favillakey.io,-27.4705,153.026
relay.sigit.io,50.4754,12.3683
nrl.ceskar.xyz,50.5145,16.0119
nostr.chaima.info,50.1109,8.68213
nostr.janx.com,43.6532,-79.3832
relay.pornstr.net,53.3498,-6.26031
nostr-02.yakihonne.com,1.32123,103.695
test.relays.nuts.cash,52.3676,4.90414
auth.nostr1.com,40.7057,-74.0136
relay2.angor.io,48.1046,11.6002
nostr-01.yakihonne.com,1.32123,103.695
relay.paulstephenborile.com:443,49.4543,11.0746
relay.dwadziesciajeden.pl,52.2297,21.0122
relay.primal.net,43.6532,-79.3832
nostr2.girino.org:443,43.6532,-79.3832
relay.getvia.xyz,60.1699,24.9384
relay.degmods.com,50.4754,12.3683
relay.edufeed.org,49.4521,11.0767
relay.ltgnet.work:8443,51.0511,-114.075
chorus.mikedilger.com:444,-36.8906,174.794
bitchat.nostr1.com,40.7057,-74.0136
nostrrelay.taylorperron.com,45.5029,-73.5723
temp.iris.to,43.6532,-79.3832
relay.ditto.pub,43.6532,-79.3832
nostr-relay.zimage.com,34.282,-118.439
prl.plus,54.9486,21.7283
relay.satsmarkt.club,52.3676,4.90414
nostr.azzamo.net,52.2633,21.0283
nostrelay.circum.space:443,52.2245,8.826
relay.nostrhub.fr,48.1045,11.6004
relay.lightning.pub,39.0438,-77.4874
relay.artx.market,43.6548,-79.3885
nostr.jcera.eu,43.6532,-79.3832
nostr.mom,50.4754,12.3683
relay.plebdevs.com,40.7128,-74.006
nostr-dev.wellorder.net,45.5201,-122.99
relay.fundstr.me,42.3601,-71.0589
21milionidinostr.duckdns.org,45.519,9.08615
nostr.alex71btc.com,43.6532,-79.3832
chat-relay.zap-work.com:443,43.6532,-79.3832
relay.qstr.app,50.1109,8.68213
nostr.middling.mydns.jp,35.8099,140.12
relayone.geektank.ai:443,39.0997,-94.5786
relay.shychat.lol,39.8965,-80.1792
relay.novospes.com,43.6532,-79.3832
relay.bitos.space,43.6532,-79.3832
nostr.rtvslawenia.com,49.4543,11.0746
relay.twentyone.ist,43.7696,-79.3309
relay2.nostrasia.net,43.6532,-79.3832
relay-dev.satlantis.io,39.0438,-77.4874
testnet.samt.st,43.6532,-79.3832
nostr-relay.corb.net,39.6478,-104.988
relay.bullishbounty.com,43.6532,-79.3832
relay.nostr.space,43.6532,-79.3832
relay-dev.gulugulu.moe,43.6532,-79.3832
relay.illuminodes.com,43.6532,-79.3832
nos.lol,50.4754,12.3683
nostr.trevena.info,43.6532,-79.3832
support.flotilla.social,32.9483,-96.7299
no.str.cr:443,8.96171,-83.5246
relay.satlantis.io,39.0438,-77.4874
nostrelay.circum.space,52.2245,8.826
nostr.tbxnetworx.de,47.8267,10.0205
vault.iris.to,43.6532,-79.3832
relay.hazeem.xyz,43.6532,-79.3832
n.kojira.io,43.6532,-79.3832
relay.kilombino.com,43.6532,-79.3832
relay.nostr.net,43.6532,-79.3832
relay.nostrasia.net,43.6532,-79.3832
buzz.cashu.space,43.6532,-79.3832
greenart7c3.feeds.relay.tools,38.6327,-90.1961
nostr.spaceshell.xyz,43.6532,-79.3832
relay.nostriot.com,41.5695,-83.9786
relay.snort.social,53.3498,-6.26031
relay.nostrcade.com,37.79,-122.401
groups.beginningend.com,35.2227,-97.4786
nostr.self-determined.de,51.4379,14.2355
relay.aarpia.com,37.3986,-121.964
relay.underorion.se,51.4998,-0.011072
relay.staging.plebeian.market:443,51.5072,-0.127586
relay.chorus.community,50.1109,8.68213
dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832
relay.staging.plebeian.market,51.5072,-0.127586
relay.snotr.nl:49999,51.9758,4.31389
baunilha.ie.apps.lnvps.cloud,53.3498,-6.26031
n.bogs.ccwu.cc,43.6532,-79.3832
relay.angor.io,48.1046,11.6002
nostr.2b9t.xyz,34.0549,-118.243
armada.sharegap.net,43.6532,-79.3832
nostr.chaima.info,51.5072,-0.127586
nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832
relay1.privkey.io,25.7617,-80.1918
yabu.me,35.6096,139.73
relay.samt.st,40.8302,-74.1299
ribo.eu.nostria.app:443,43.6532,-79.3832
relay.lightning.pub,39.0438,-77.4874
relay.nostu.be,40.4167,-3.70329
nostr.whitenode45.ddns.net,40.55,-74.4758
nostr.carroarmato0.be:443,50.914,3.21378
cdn.satellite.earth,40.8302,-74.1299
nos.lol:443,50.4754,12.3683
relay.nostrian-conquest.com,41.223,-111.974
vault.iris.to:443,43.6532,-79.3832
relay.orangem.art,43.6532,-79.3832
relay.satmaxt.xyz,43.6532,-79.3832
relay.mwaters.net,50.9871,2.12554
relayrs.notoshi.win:443,43.6532,-79.3832
relay-na1.metanomalist.com,43.6532,-79.3832
relay.piazza.today,48.122,11.589
relay.wavlake.com,41.2619,-95.8608
nostr-relay.xbytez.io:443,50.6924,3.20113
relay.nostrverse.net,43.6532,-79.3832
nostr.malin.onl,43.6532,-79.3832
budabit.nostr1.com,40.7057,-74.0136
bridge.tagomago.me,42.3601,-71.0589
relay.bullishbounty.com:443,43.6532,-79.3832
wheat.oslim.dev,43.6532,-79.3832
nostr.planix.org,43.6532,-79.3832
nostr.zoracle.org,45.6018,-121.185
spurline.safebox.dev,43.6532,-79.3832
relay.chorus.community:443,50.1109,8.68213
nostr.wecsats.io,43.6532,-79.3832
relay.oranjkid.com,41.8781,-87.6298
offchain.bostr.online,43.6532,-79.3832
relay.wavlake.com:443,41.2619,-95.8608
nostr.trepechov.com,49.4543,11.0746
relay.paulstephenborile.com,49.4543,11.0746
relay2.veganostr.com,60.1699,24.9384
relay.beginningend.com,35.2227,-97.4786
nostr.data.haus,50.4754,12.3683
bitcoiner.social,47.6743,-117.112
relay.jmoose.rocks:443,32.7767,-96.797
strfry.shock.network,39.0438,-77.4874
relay.minibolt.info,43.6532,-79.3832
relay.nostrfeed.com,60.1699,24.9384
relay.layer.systems:443,49.0291,8.35695
relay0.gfcom.info,13.7653,100.647
relay.mmwaves.de:443,48.8575,2.35138
offchain.pub,39.1585,-94.5728
bcast.girino.org,43.6532,-79.3832
staging.yabu.me,35.6092,139.73
nostr.overpay.com,29.7449,-95.5343
bridge.tagomago.me,42.3601,-71.0589
nostr-01.yakihonne.com,1.32123,103.695
strfry.bonsai.com,39.0438,-77.4874
relay2.0xprivacy.online,43.6532,-79.3832
relay.conduit.market,38.7946,-106.535
nostr.pbfs.io,50.4754,12.3683
relay.noeudlibre.fr,50.6924,3.20113
relay.earthly.city,34.1749,-118.54
relay.veganostr.com,60.1699,24.9384
relay.cypherguide.org,1.32123,103.695
relay.routstr.com,59.4016,17.9455
relay.nostrnostr.net,43.6532,-79.3832
no.str.cr,8.96171,-83.5246
relay.littlebitstudios.com,34.8688,-86.5502
relay.decentnewsroom.com,50.4754,12.3683
zencrown.relay2aeon.quest,52.3061,4.6907
cs-relay.nostrdev.com:443,50.4754,12.3683
relay.nostrcheck.me,43.6532,-79.3832
nostr-relayflare.enpitsulin.workers.dev,43.6532,-79.3832
relay.jcera.eu,43.6532,-79.3832
soloco.nl,43.6532,-79.3832
nostr.linky.fit,50.1109,8.68213
relay.nostrops.com,32.71,-96.6745
nostr.nestoris.online,55.568,37.5853
cache.trustr.ing,43.6548,-79.3885
denimroad.feeds.relay.tools,38.6327,-90.1961
nostr.easycryptosend.it,43.6532,-79.3832
nostr.overmind.lol,43.6532,-79.3832
relay.lacrypta.ar,43.6532,-79.3832
torly.smesh.lol,32.7767,-96.797
nostr.wild-vibes.ts.net,48.8566,2.35222
nostr.christiansass.de,51.1798,7.1925
nostr.eu.freedomrelay.dev,49.0524,14.4347
ec1.f7z.io,60.1699,24.9384
relay.selfrule.co.za,43.6532,-79.3832
bbw-nostr.xyz,34.0549,-118.243
slick.mjex.me,39.0418,-77.4744
0x-nostr-relay.fly.dev,38.7946,-106.535
bucket.coracle.social,37.7775,-122.397
relay.44billion.net,43.6532,-79.3832
relayrs.notoshi.win,43.6532,-79.3832
nostr-01.uid.ovh,50.9871,2.12554
strfry.bonsai.com:443,39.0438,-77.4874
porchlight.social,43.6532,-79.3832
fanfares.nostr1.com,40.7057,-74.0136
borgar.lol,52.3676,4.90414
nostr.bitcoiner.social:443,47.6743,-117.112
nostr.bitcoiner.social,47.6743,-117.112
nostr.chaima.info:443,50.1109,8.68213
nostr-relay.corb.net:443,39.6478,-104.988
relay-can.zombi.cloudrodion.com,43.6532,-79.3832
nos.xmark.cc,50.6916,3.20151
relay02.lnfi.network,35.6764,139.65
talon.quest,43.6532,-79.3832
relay.substation.ninja,43.6532,-79.3832
nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874
relay.sharegap.net,43.6532,-79.3832
nostr.now,35.8099,140.12
relayone.geektank.ai,39.0997,-94.5786
relay.nostr.black,43.6532,-79.3832
relay.cyberguy.fyi,52.3676,4.90414
relay.wisp.talk,49.4543,11.0746
relay.mostro.network,40.8302,-74.1299
relay.cloistr.xyz,42.337,-77.3178
chat-relay.zap-work.com,43.6532,-79.3832
relay.wasabiwallet.io,43.6532,-79.3832
relay.atsocy.com,43.6532,-79.3832
articles.layer3.news,37.3387,-121.885
scores.brainstorm.world,39.0997,-94.5786
relay.rebelbase.social,50.965,5.50082
purplerelay.com,43.6532,-79.3832
relay.pprgb.app,43.6532,-79.3832
relay.nostrmap.net,60.1699,24.9384
ynostr.yael.at,60.1699,24.9384
nostr.mom:443,50.4754,12.3683
vidar.nestoris.online,52.3676,4.90414
nostr.trcjr.xyz,35.3821,-78.5486
relay.directsponsor.net,42.8864,-78.8784
relay.duck1123.com,43.6532,-79.3832
relay-dev.gulugulu.moe:443,43.6532,-79.3832
relay.i9.eti.br,43.6532,-79.3832
nostriches.club,43.6532,-79.3832
relay.loveisbitcoin.com,43.6532,-79.3832
relay.openspecs.uid.ovh,50.9871,2.12554
relay.getsafebox.app,43.6532,-79.3832
relay.lanacoin-eternity.com,40.8302,-74.1299
nostr.davenov.com,50.1109,8.68213
relay.tapestry.ninja,40.8054,-74.0241
nostr2.girino.org,43.6532,-79.3832
relay.zone667.com,60.1699,24.9384
nostr.rtvslawenia.com:443,49.4543,11.0746
relay.mccormick.cx,52.3563,4.95714
nostr-relay.cbrx.io,43.6532,-79.3832
nip85.nosfabrica.com,39.0997,-94.5786
dev-relay.nostr.space,43.6532,-79.3832
spamspamspamspam.rest,43.6532,-79.3832
relay.gorrdy.cz,50.1109,8.68213
nostr.bitcoinx.gr,49.0291,8.35696
nostr.sovereignservices.xyz,43.6532,-79.3832
relay.wisp.talk:443,49.4543,11.0746
top.testrelay.top,43.6532,-79.3832
relay.jmoose.rocks,32.7767,-96.797
nostr.relay.hedwig.sh,60.1699,24.9384
adre.su,59.9311,30.3609
nostr.tabordalab.com,60.1699,24.9384
relay.nostrconnect.com,41.5695,-83.9786
relay.hjkl.zip,43.6532,-79.3832
freelay.sovbit.host,60.1699,24.9384
syb.lol,43.6532,-79.3832
relay.nearhood.co.uk,51.5134,-0.0890675
nostr.snowbla.de:443,50.4754,12.3683
relay.getsafebox.app:443,43.6532,-79.3832
relay.inforsupports.com,43.6532,-79.3832
nostr-relay.xbytez.io,50.6924,3.20113
fanfares.nostr1.com:443,40.7057,-74.0136
nostr.islandarea.net,35.4669,-97.6473
dm-test-strfry-generic.samt.st,43.6532,-79.3832
treuzkas.branruz.com,48.8575,2.35138
relay-rpi.edufeed.org:443,49.4521,11.0767
vault.iris.to:443,43.6532,-79.3832
node.kommonzenze.de,49.4521,11.0767
nostr.stakey.net:443,52.6485,4.73787
nostr.thalheim.io:443,60.1699,24.9384
soloco.nl,43.6532,-79.3832
strfry.shock.network,39.0438,-77.4874
nostr-relay.zimage.com,34.0549,-118.243
public.crostr.com:443,43.6532,-79.3832
nostr.sathoarder.com:443,48.5734,7.75211
relay.angor.io,48.1046,11.6002
relay.wellorder.net,45.5201,-122.99
relay.mwaters.net,50.9871,2.12554
relay.staging.commonshub.brussels,49.4543,11.0746
nostr-verified.wellorder.net,45.5201,-122.99
nostr-pub.wellorder.net,45.5201,-122.99
nostr-2.21crypto.ch,47.5356,8.73209
relay.kaleidoswap.com,50.8476,4.35717
relay.libernet.app:443,43.6532,-79.3832
relay.homeinhk.xyz,35.694,139.754
relay.manneken.brussels,49.4543,11.0746
nostr.spicyz.io:443,43.6532,-79.3832
relay.lanacoin-eternity.com:443,40.8302,-74.1299
ribo.us.nostria.app:443,43.6532,-79.3832
relay.loveisbitcoin.com,43.6532,-79.3832
relay.angor.io:443,48.1046,11.6002
relay02.lnfi.network,35.6764,139.65
relay.cosmicbolt.net:443,37.3986,-121.964
nostr-rs-relay-qj1h.onrender.com,37.7775,-122.397
nrs-01.darkcloudarcade.com,39.0997,-94.5786
relay.endfiat.money:443,59.3327,18.0656
relay.paulstephenborile.com,49.4543,11.0746
rele.speyhard.fi,51.5072,-0.127586
relay.froth.zone,60.1699,24.9384
relay.nostr.blockhenge.com,39.0438,-77.4874
nrl.ceskar.xyz,50.5145,16.0119
rilo.nostria.app,43.6532,-79.3832
nostr.overmind.lol:443,43.6532,-79.3832
nostr.snowbla.de:443,50.4754,12.3683
nostrrelay.taylorperron.com,45.5029,-73.5723
chorus.pjv.me,45.5201,-122.99
relay.nostr.place,43.6532,-79.3832
bucket.coracle.social,37.7775,-122.397
nostr.girino.org:443,43.6532,-79.3832
relay.aarpia.com,37.3986,-121.964
nostr.thalheim.io,60.1699,24.9384
ec2.f7z.io,60.1699,24.9384
relay.trotters.cc,43.6532,-79.3832
relay.mccormick.cx:443,52.3563,4.95714
relay.momostr.pink,43.6532,-79.3832
relay.nostr.net,43.6532,-79.3832
conduitl2.fly.dev,37.7648,-122.432
chat-relay.zap-work.com,43.6532,-79.3832
relay.ditto.pub,43.6532,-79.3832
relay.veganostr.com,60.1699,24.9384
relay.minibolt.info:443,43.6532,-79.3832
adre.su,59.9311,30.3609
bitcoinostr.duckdns.org,41.1976,1.11167
nostr.computingcache.com:443,34.0356,-118.442
relay-fra.zombi.cloudrodion.com,48.8566,2.35222
nostr.hekster.org:443,37.3986,-121.964
nostr.88mph.life,52.1941,-2.21905
wot.dergigi.com,64.1476,-21.9392
nostr.planix.org,43.6532,-79.3832
relay.satsmarkt.club,52.6907,4.8181
nostrcity-club.fly.dev:443,37.7648,-122.432
aeon.libretechsystems.xyz,55.486,9.86577
testnet.samt.st,43.6532,-79.3832
nostr.data.haus,50.4754,12.3683
wot.sudocarlos.com,43.6532,-79.3832
relay-fra.zombi.cloudrodion.com:443,48.8566,2.35222
shu01.shugur.net,21.4902,39.2246
relay.gulugulu.moe:443,43.6532,-79.3832
relay2.angor.io:443,48.1046,11.6002
relay.libernet.app,43.6532,-79.3832
directories-safe-motherboard-recipients.trycloudflare.com,43.6532,-79.3832
wot.nostr.party,36.1659,-86.7844
relay.zone667.com,60.1699,24.9384
nostr.wild-vibes.ts.net,48.8566,2.35222
relay.nostr.com,50.1109,8.68213
nostr.iskarion.ddns.net,43.3076,-2.95421
relay-dev.satlantis.io,39.0438,-77.4874
relay.sovereignresonance.org,48.9006,2.25929
relay.nostrian-conquest.com,41.223,-111.974
relay.aidatanorge.no,43.6532,-79.3832
strfry.apps3.slidestr.net,40.4167,-3.70329
relay.klabo.world,47.2343,-119.853
nostr.data.haus:443,50.4754,12.3683
testr.nymble.world,40.8054,-74.0241
relay.inforsupports.com,43.6532,-79.3832
relay.nostrmap.net:443,60.1699,24.9384
nostr.stakey.net:443,52.3676,4.90414
dev-relay.nostreon.com,60.1699,24.9384
nostr.islandarea.net:443,35.4669,-97.6473
nostr.rtvslawenia.com,49.4543,11.0746
relay.bowlafterbowl.com,32.9483,-96.7299
nostr.quali.chat:443,60.1699,24.9384
relay.plebeian.market,50.1109,8.68213
relay-rpi.edufeed.org,49.4521,11.0767
r.0kb.io,32.789,-96.7989
nostr.notribe.net:443,40.8302,-74.1299
relay.getsafebox.app:443,43.6532,-79.3832
nostr.dlcdevkit.com:443,40.0992,-83.1141
nostrelites.org,34.9582,-81.9907
nostr.hoppe-relay.it.com,42.8864,-78.8784
nostr.thebiglake.org,32.71,-96.6745
nostr-kyomu-haskell.onrender.com,37.7775,-122.397
relay.nostriot.com,41.5695,-83.9786
nostr.christiansass.de,51.7634,7.8887
relay.btcforplebs.com,43.6532,-79.3832
nostr.tagomago.me,42.3601,-71.0589
relayone.geektank.ai,39.0997,-94.5786
relay.dreamith.to:443,43.6532,-79.3832
nostr.liberty.fans,36.8767,-89.5879
wot.makenomistakes.ca,43.7064,-79.3986
relay.goodmorningbitcoin.com,43.6532,-79.3832
relay.layer.systems,49.0291,8.35695
relay.paulstephenborile.com:443,49.4543,11.0746
relay.ohstr.com,43.6532,-79.3832
nostr-relay.xbytez.io:443,50.6924,3.20113
nostr.ac,38.958,-77.3592
ribo.us.nostria.app,43.6532,-79.3832
nostr.21crypto.ch,47.5356,8.73209
relay.chorus.community:443,48.5333,10.7
relay.cypherflow.ai,48.8575,2.35138
relay.agorist.space:443,52.3734,4.89406
relay.nostrian-conquest.com:443,41.223,-111.974
relay.keykeeper.world,40.7824,-74.0711
relay.getvia.xyz,60.1699,24.9384
relay.nuts.cash,52.3676,4.90414
kotukonostr.onrender.com,37.7775,-122.397
relay.minibolt.info,43.6532,-79.3832
relay.dwadziesciajeden.pl,52.2297,21.0122
relay.fountain.fm:443,43.6532,-79.3832
relay.fountain.fm,43.6532,-79.3832
nostr-02.uid.ovh,50.9871,2.12554
relay.lanavault.space:443,60.1699,24.9384
nostr.carroarmato0.be,50.914,3.21378
nexus.libernet.app:443,43.6532,-79.3832
relay.artio.inf.unibe.ch,46.9501,7.43678
blossom.gnostr.cloud,43.6532,-79.3832
relay.binaryrobot.com,43.6532,-79.3832
relay.earthly.city,34.1749,-118.54
nostr.hifish.org,47.4244,8.57658
offchain.pub:443,39.1585,-94.5728
relay.bullishbounty.com:443,43.6532,-79.3832
strfry.openhoofd.nl:443,51.5717,3.70417
cs-relay.nostrdev.com:443,50.4754,12.3683
strfry.ymir.cloud,43.6532,-79.3832
nostrbtc.com,43.6532,-79.3832
relay.directsponsor.net,42.8864,-78.8784
nostr2.girino.org,43.6532,-79.3832
relay.sigit.io:443,50.4754,12.3683
relay.getsafebox.app,43.6532,-79.3832
antiprimal.net,43.6532,-79.3832
nostr.sathoarder.com,48.5734,7.75211
inbox.scuba323.com,40.8218,-74.45
nrs-01.darkcloudarcade.com:443,39.0997,-94.5786
nostr.tac.lol,47.4748,-122.273
nostr.davenov.com,50.1109,8.68213
relay.trotters.cc:443,43.6532,-79.3832
nostr.plantroon.com:443,50.1013,8.62643
relay.nostreon.com,60.1699,24.9384
nostr.easycryptosend.it,43.6532,-79.3832
nostr-01.yakihonne.com:443,1.32123,103.695
relay-testnet.k8s.layer3.news,37.3387,-121.885
nostr.purpura.cloud,43.6532,-79.3832
insta-relay.apps3.slidestr.net,40.4167,-3.70329
nostr.mifen.me,43.6532,-79.3832
testnet-relay.samt.st:443,40.8302,-74.1299
nostr.2b9t.xyz:443,34.0549,-118.243
relay.wavlake.com:443,41.2619,-95.8608
relay.wisp.talk:443,49.4543,11.0746
relay-dev.satlantis.io:443,39.0438,-77.4874
relay.satlantis.io,39.0438,-77.4874
relay.staging.plebeian.market,51.5072,-0.127586
relay.openfarmtools.org,60.1699,24.9384
relay.nostrhub.fr,48.1045,11.6004
nostr-relay.xbytez.io,50.6924,3.20113
relay.binaryrobot.com:443,43.6532,-79.3832
relay.samt.st,40.8302,-74.1299
relay.illuminodes.com,43.6532,-79.3832
relay.liberbitworld.org,43.6532,-79.3832
relay.olas.app:443,60.1699,24.9384
no.str.cr,8.96171,-83.5246
dm-test-strfry-discovery.samt.st,43.6532,-79.3832
wot.rejecttheframe.xyz,43.6532,-79.3832
relay.nostriot.com:443,41.5695,-83.9786
nostr.plantroon.com,50.1013,8.62643
nostr-01.uid.ovh,50.9871,2.12554
relay.openresist.com:443,43.6532,-79.3832
nostr.overmind.lol,43.6532,-79.3832
relay.internationalright-wing.org,-22.5022,-48.7114
nostr.myshosholoza.co.za:443,52.3676,4.90414
nostr.pbfs.io:443,50.4754,12.3683
21milionidinostr.duckdns.org,41.8967,12.4822
nostr.4rs.nl,49.0291,8.35696
relay.lanavault.space,60.1699,24.9384
relay.mostr.pub,43.6532,-79.3832
relay.nostar.org,43.6532,-79.3832
nostr.mom,50.4754,12.3683
relay.decentralia.fr,48.122,11.589
nostrcheck.me,43.6532,-79.3832
relay.agentry.com,42.8864,-78.8784
relay2.angor.io,48.1046,11.6002
slick.mjex.me,39.0418,-77.4744
relay-us.zombi.cloudrodion.com,40.7862,-74.0743
relay.vrtmrz.net:443,43.6532,-79.3832
relay.beginningend.com,35.2227,-97.4786
chat-relay.zap-work.com:443,43.6532,-79.3832
relay.underorion.se,50.1109,8.68213
relay.mitchelltribe.com,39.0438,-77.4874
relay.qstr.app,51.5072,-0.127586
relay.cyberguy.fyi,52.6907,4.8181
strfry.bonsai.com:443,39.0438,-77.4874
relayone.soundhsa.com:443,39.0997,-94.5786
relay.sigit.io,50.4754,12.3683
relay.npubhaus.com,43.6532,-79.3832
relayrs.notoshi.win,43.6532,-79.3832
relay.mitchelltribe.com:443,39.0438,-77.4874
relay.44billion.net,43.6532,-79.3832
reraw.pbla2fish.cc,43.6532,-79.3832
articles.layer3.news:443,37.3387,-121.885
nostr.sovereignservices.xyz,43.6532,-79.3832
relay.nostx.io,43.6532,-79.3832
nostr-relay.amethyst.name,39.0067,-77.4291
0x-nostr-relay.fly.dev,37.7648,-122.432
relay.ohstr.com:443,43.6532,-79.3832
00f2e774.relay.dev.thunderegg.us,39.0438,-77.4874
nostr-relay.cbrx.io,43.6532,-79.3832
relay.wavlake.com,41.2619,-95.8608
purplerelay.com:443,43.6532,-79.3832
nostr-pr02.redscrypt.org,52.3676,4.90414
fanfares.nostr1.com:443,40.7057,-74.0136
kasztanowa.bieda.it,43.6532,-79.3832
relay.flashapp.me,43.6548,-79.3885
relay.typedcypher.com,51.5072,-0.127586
nostr.bond,50.1109,8.68213
nostr.azzamo.net,52.2633,21.0283
nexus.libernet.app,43.6532,-79.3832
relay.cosmicbolt.net,37.3986,-121.964
schnorr.me,43.6532,-79.3832
relay.mostro.network:443,40.8302,-74.1299
relay-arg.zombi.cloudrodion.com,1.35208,103.82
relay.chorus.community,48.5333,10.7
blossom.gnostr.cloud:443,43.6532,-79.3832
syb.lol:443,34.0549,-118.243
relay.dyne.org,49.0291,8.35705
btc.klendazu.com,41.2861,1.24993
wot.nostr.place,43.6532,-79.3832
relay.openresist.com,43.6532,-79.3832
rilo.nostria.app:443,43.6532,-79.3832
no.str.cr:443,8.96171,-83.5246
relay.mostr.pub:443,43.6532,-79.3832
relay.edufeed.org:443,49.4521,11.0767
nostr.debate.report,50.1109,8.68213
relay.satmaxt.xyz:443,43.6532,-79.3832
relay.artx.market:443,43.6548,-79.3885
relay-dev.gulugulu.moe,43.6532,-79.3832
relay.novospes.com,43.6532,-79.3832
relay.nostr-check.me,43.6532,-79.3832
nostr.computingcache.com,34.0356,-118.442
nostr.oxtr.dev,50.4754,12.3683
relay.fckstate.net,59.3293,18.0686
relay.vrtmrz.net,43.6532,-79.3832
relay.bornheimer.app,51.5072,-0.127586
relay.guggero.org,46.5971,9.59652
relay01.lnfi.network,35.6764,139.65
wot.shaving.kiwi,43.6532,-79.3832
nostr.twinkle.lol,51.902,7.6657
relay.edufeed.org,49.4521,11.0767
relay.lanacoin-eternity.com,40.8302,-74.1299
relay.satmaxt.xyz,43.6532,-79.3832
nostr.hifish.org:443,47.4244,8.57658
relay.cypherflow.ai:443,48.8575,2.35138
infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832
nostr.na.social:443,43.6532,-79.3832
nostr.rtvslawenia.com:443,49.4543,11.0746
relay.mypathtofire.de,42.8864,-78.8784
public.crostr.com,43.6532,-79.3832
relay.olas.app,60.1699,24.9384
relay.agora.social,50.7383,15.0648
ribo.nostria.app,43.6532,-79.3832
relay.lab.rytswd.com,49.4543,11.0746
relay.ditto.pub:443,43.6532,-79.3832
porchlight.social,43.6532,-79.3832
nostr.notribe.net,40.8302,-74.1299
relay.endfiat.money,59.3327,18.0656
nostr.myshosholoza.co.za,52.3676,4.90414
relay.nearhood.co.uk,51.5134,-0.0890675
relay.degmods.com,50.4754,12.3683
nostr.novacisko.cz,52.2026,20.9397
prl.plus,55.7628,37.5983
bruh.samt.st,43.6532,-79.3832
strfry.openhoofd.nl,51.5717,3.70417
nostr.spicyz.io,43.6532,-79.3832
nostr.na.social,43.6532,-79.3832
nip85.nosfabrica.com,39.0997,-94.5786
premium.primal.net,43.6532,-79.3832
fanfares.nostr1.com,40.7057,-74.0136
relay.scuba323.com,40.8218,-74.45
nostr2.girino.org:443,43.6532,-79.3832
relay.mmwaves.de,48.8575,2.35138
nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874
strfry.shock.network:443,39.0438,-77.4874
nostr.snowbla.de,50.4754,12.3683
nostr.spaceshell.xyz,43.6532,-79.3832
nostr.quali.chat,60.1699,24.9384
wot.utxo.one,43.6532,-79.3832
relay.mccormick.cx,52.3563,4.95714
mostro-p2p.tech,50.1109,8.68213
basspistol.org,49.0291,8.35696
ribo.nostria.app:443,43.6532,-79.3832
chorus.mikedilger.com:444,-36.8906,174.794
nostr.oxtr.dev:443,50.4754,12.3683
nostr.nodesmap.com,59.3327,18.0656
offchain.bostr.online,43.6532,-79.3832
purplerelay.com,43.6532,-79.3832
relayrs.notoshi.win:443,43.6532,-79.3832
relay.wavefunc.live,41.8781,-87.6298
relay.dreamith.to,43.6532,-79.3832
bendernostur.duckdns.org:8443,50.1109,8.68213
relay.nmail.li,50.9871,2.12554
nostr-relay.corb.net,39.6478,-104.988
relay.staging.plebeian.market:443,51.5072,-0.127586
spamspamspamspam.rest,43.6532,-79.3832
relay1.gfcom.info,13.9215,100.538
schnorr.me:443,43.6532,-79.3832
relay.lab.rytswd.com:443,49.4543,11.0746
nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874
dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832
relay.nostrmap.net,60.1699,24.9384
nostr.relay.hedwig.sh,60.1699,24.9384
relay.veganostr.com:443,60.1699,24.9384
relay.wavefunc.live:443,41.8781,-87.6298
nostr.mikoshi.de,52.52,13.405
syb.lol,34.0549,-118.243
relay1.nostrchat.io,60.1699,24.9384
nostr.wecsats.io:443,43.6532,-79.3832
nostr.chaima.info:443,51.5072,-0.127586
nostr.azzamo.net:443,52.2633,21.0283
relay-can.zombi.cloudrodion.com,43.6532,-79.3832
nostr.unkn0wn.world,46.8499,9.53287
relayone.soundhsa.com,39.0997,-94.5786
x.kojira.io,43.6532,-79.3832
dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832
nostrelay.circum.space,52.6907,4.8181
relay.primal.net,43.6532,-79.3832
nostr.girino.org,43.6532,-79.3832
nostr.pbfs.io,50.4754,12.3683
relay.kalcafe.xyz,37.3986,-121.964
relay.gulugulu.moe,43.6532,-79.3832
top.testrelay.top,43.6532,-79.3832
relay.kilombino.com,43.6532,-79.3832
nos.lol:443,50.4754,12.3683
nos.lol,50.4754,12.3683
relay.nostr.place:443,43.6532,-79.3832
cache.trustr.ing,43.6548,-79.3885
relay.internationalright-wing.org:443,-22.5022,-48.7114
relay.laantungir.net,-19.4692,-42.5315
relay.lightning.pub:443,39.0438,-77.4874
nostr.stakey.net,52.3676,4.90414
articles.layer3.news,37.3387,-121.885
relay.wisp.talk,49.4543,11.0746
relay.pyramid.li,47.4093,8.46503
relay.typedcypher.com:443,51.5072,-0.127586
dev.relay.stream,43.6532,-79.3832
relay.bullishbounty.com,43.6532,-79.3832
nostr.mom:443,50.4754,12.3683
relay.plebeian.market:443,50.1109,8.68213
nostr.hekster.org,37.3986,-121.964
nostrcity-club.fly.dev,37.7648,-122.432
nostr.vulpem.com,49.4543,11.0746
relay-dev.gulugulu.moe:443,43.6532,-79.3832
weboftrust.libretechsystems.xyz,55.4724,9.87335
nostr-relay.corb.net:443,39.6478,-104.988
wheat.happytavern.co,43.6532,-79.3832
relay.mappingbitcoin.com,43.6532,-79.3832
testnet-relay.samt.st,40.8302,-74.1299
relay.bitmacro.cloud,43.6532,-79.3832
dev.relay.edufeed.org,49.4521,11.0767
myvoiceourstory.org,37.3598,-121.981
relay.stickeroo.is-cool.dev,37.3387,-121.885
relay.agorist.space,52.3734,4.89406
freelay.sovbit.host,60.1699,24.9384
nostr-dev.wellorder.net,45.5201,-122.99
nostr.middling.mydns.jp,35.8099,140.12
cs-relay.nostrdev.com,50.4754,12.3683
x.kojira.io:443,43.6532,-79.3832
nostrelay.circum.space:443,52.6907,4.8181
nostr.janx.com,43.6532,-79.3832
relay.mrmave.work,43.6532,-79.3832
espelho.girino.org,43.6532,-79.3832
hol.is,43.6532,-79.3832
ribo.eu.nostria.app,43.6532,-79.3832
nostr.yutakobayashi.com,43.6532,-79.3832
relay.mostro.network,40.8302,-74.1299
communities.nos.social,40.8302,-74.1299
relay.solife.me,43.6532,-79.3832
yabu.me,35.6092,139.73
relay.islandbitcoin.com,12.8498,77.6545
nostr.wecsats.io,43.6532,-79.3832
nostr.tac.lol:443,47.4748,-122.273
relay.arx-ccn.com,50.4754,12.3683
nostrride.io,37.3986,-121.964
r.0kb.io:443,32.789,-96.7989
herbstmeister.com,34.0549,-118.243
relay.artx.market,43.6548,-79.3885
vault.iris.to,43.6532,-79.3832
relay.ru.ac.th,13.7607,100.627
temp.iris.to,43.6532,-79.3832
social.amanah.eblessing.co,48.1046,11.6002
nostr-relay.nextblockvending.com,47.2343,-119.853
wot.codingarena.top,50.4754,12.3683
relay.sincensura.org,43.6532,-79.3832
nostr.dlcdevkit.com,40.0992,-83.1141

1 Relay URL Latitude Longitude
relay.agentry.com 42.8864 -78.8784
relay.veganostr.com:443 60.1699 24.9384
relay.nostrfy.org 35.6916 139.768
nostr.debate.report 50.1109 8.68213
relay.nostr.blockhenge.com 39.0438 -77.4874
memlay.v0l.io 53.3498 -6.26031
relay.monostr.com 50.4754 12.3683
nostr.emanuelemiani.it 45.778 8.79414
nostr.hifish.org 47.4009 8.57814
nostr.aphelion.network 55.7623 37.6381
nostr.synalysis.com 50.4754 12.3683
nip85-staging.nosfabrica.com 39.0997 -94.5786
relay.bnos.space 43.6532 -79.3832
nostr.islandarea.net 35.4669 -97.6473
nostr.tac.lol 47.4748 -122.273
nostr.plantroon.com:443 50.1013 8.62643
nostr-pub.wellorder.net 45.5201 -122.99
relay.ru.ac.th 13.7607 100.627
conduit-congee.fly.dev 38.7946 -106.535
nostr-verified.wellorder.net 45.5201 -122.99
nostr.girino.org 43.6532 -79.3832
nostr.stakey.net 52.6485 4.73787
relay.satsapp.me 50.4754 12.3683
nostr.tagomago.me 42.3601 -71.0589
nostrcity-club.fly.dev 38.7946 -106.535
relay.dyne.org 49.0291 8.35705
nostr.liberty.fans 36.8767 -89.5879
relay.agora.social 50.7383 15.0648
relay.lightning.pub:443 39.0438 -77.4874
relay.thewired.app 39.0438 -77.4874
relay.s-w.art 43.6532 -79.3832
relay5.bitransfer.org 43.6532 -79.3832
nostr.computingcache.com 45.5341 -122.956
openrelay.ziomc.com 41.8967 12.4822
relayone.soundhsa.com 39.0997 -94.5786
nostr-rs-relay-ishosta.phamthanh.me 43.6532 -79.3832
relay.nmail.li 50.9871 2.12554
relay.islandbitcoin.com 20.3898 78.0903
hasky.chat 40.7862 -74.0743
relay.mitosystem.net -23.5558 -46.6396
nostr.oxtr.dev 50.4754 12.3683
staging.yabu.me 35.6096 139.73
nostr.dzo-hadar.ts.net 35.6764 139.65
relayone.soundhsa.com:443 39.0997 -94.5786
nostr.benderhost.org 53.3498 -6.26031
relay.dreamith.to 43.6532 -79.3832
nostr.n7ekb.net 47.4941 -122.294
relay.nostr.com 50.1109 8.68213
relay.layer.systems 43.6532 -79.3832
relay.budabit.club 49.4543 11.0746
relay2.orangesync.tech 40.7128 -74.006
r.senx.io 50.6924 3.20113
relay.artiostr.ch 43.6532 -79.3832
relay.fiatdenier.com 43.7787 -79.5393
relay.btcforplebs.com 43.6532 -79.3832
nostr.islandarea.net:443 35.4669 -97.6473
cs-relay.nostrdev.com 50.4754 12.3683
public.crostr.com:443 43.6532 -79.3832
relay.nostrmap.net:443 60.1699 24.9384
relay.chatbett.de 40.7128 -74.006
relay.shadowbip.com 54.352 18.6466
nostr.snowbla.de 50.4754 12.3683
portal-relay.pareto.space 53.5488 9.98717
relay.mypathtofire.de 42.8864 -78.8784
nostr.spicyz.io 43.6532 -79.3832
basspistol.org 49.0291 8.35696
relay.notoshi.win 13.3622 100.983
nostrride.io 43.6532 -79.3832
nostr.yutakobayashi.com 43.6532 -79.3832
nostria.at 48.3064 14.2861
relay.gulugulu.moe 43.6532 -79.3832
bruh.samt.st 43.6532 -79.3832
nostr.zeroflux.net 50.4754 12.3683
nostr.pbfs.io:443 50.4754 12.3683
dev.relay.stream 43.6532 -79.3832
nostr.21crypto.ch 47.5356 8.73209
nostr-kyomu-haskell.onrender.com 37.7775 -122.397
relay.fountain.fm 43.6532 -79.3832
relay.bowlafterbowl.com 32.9483 -96.7299
dm-test-strfry-generic.samt.st 43.6532 -79.3832
nostr.mad-social.net 50.4754 12.3683
relay.laantungir.net -19.4692 -42.5315
relay.manneken.brussels 49.4543 11.0746
nostr.hetuniversum.nl 43.6532 -79.3832
relay.lotek-distro.com 43.6532 -79.3832
nr.yay.so 46.2126 6.1154
pymd.kamp.site 38.9072 -77.0369
relay.nostar.org 43.6532 -79.3832
nostr-02.uid.ovh 50.9871 2.12554
relay.bornheimer.app 50.1109 8.68213
nostrcity-club.fly.dev:443 38.7946 -106.535
relay.hivescope.xyz 40.4332 -3.62431
relay.olas.app 60.1699 24.9384
relay.masize.com 43.6532 -79.3832
relay.edufeed.org:443 49.4521 11.0767
relay.colombiap2p.com 43.6532 -79.3832
nostr.carroarmato0.be 51.0355 4.48285
bcast.girino.org 43.6532 -79.3832
relay.ohstr.com 43.6532 -79.3832
schnorr.me:443 43.6532 -79.3832
relay.npubhaus.com 43.6532 -79.3832
relay.nostx.io 43.6532 -79.3832
relay.keykeeper.world 40.7824 -74.0711
nostr.twinkle.lol 51.902 7.6657
relay.trustr.ing 43.6548 -79.3885
offchain.pub:443 39.1585 -94.5728
dm-test-strfry-discovery.samt.st 43.6532 -79.3832
relay.liberbitworld.org 43.6532 -79.3832
dare-sydney-ethical-aspect.trycloudflare.com 43.6532 -79.3832
relay01.lnfi.network 35.6764 139.65
relay.pacsarcade.org 32.7767 -96.797
relay.nostr.dev.br 48.1046 11.6002
relay.gulugulu.moe:443 43.6532 -79.3832
relay.sincensura.org 43.6532 -79.3832
cyberspace.nostr1.com 40.7057 -74.0136
public.crostr.com 43.6532 -79.3832
relay-fra.zombi.cloudrodion.com 48.8566 2.35222
nostr.quali.chat 60.1699 24.9384
nostr-rs-relay.dev.fedibtc.com 39.0438 -77.4874
free.relayted.de 50.1109 8.68213
relay.klabo.world 47.674 -122.122
testr.nymble.world 40.8054 -74.0241
relay.czas.plus 50.1109 8.68213
relay.guggero.org 46.5971 9.59652
relay.kizuna-miki.com 35.694 139.754
nostras.app 38.7946 -106.535
nostr.pro.aw 43.6532 -79.3832
nostr.oxtr.dev:443 50.4754 12.3683
relay.crovaxon.de 53.0894 7.32917
relay.carlismo.mx 50.1109 8.68213
relay-us.zombi.cloudrodion.com 40.7862 -74.0743
relay.satmaxt.xyz:443 43.6532 -79.3832
schnorr.me 43.6532 -79.3832
relay.mappingbitcoin.com 43.6532 -79.3832
relay.t6x.host 40.8302 -74.1299
relay.lanavault.space 60.1699 24.9384
nostr.88mph.life 52.1941 -2.21905
nostr.hoppe-relay.it.com 42.8864 -78.8784
relay.hivetalk.org 40.8302 -74.1299
x.kojira.io 43.6532 -79.3832
relay.formstr.app 43.6532 -79.3832
offchain.pub 39.1585 -94.5728
chorus.bonsai.com 39.0438 -77.4874
relay.favillakey.io -27.4705 153.026
relay.sigit.io 50.4754 12.3683
nrl.ceskar.xyz 50.5145 16.0119
nostr.chaima.info 50.1109 8.68213
nostr.janx.com 43.6532 -79.3832
relay.pornstr.net 53.3498 -6.26031
nostr-02.yakihonne.com 1.32123 103.695
test.relays.nuts.cash 52.3676 4.90414
auth.nostr1.com 40.7057 -74.0136
relay2.angor.io 48.1046 11.6002
nostr-01.yakihonne.com 1.32123 103.695
relay.paulstephenborile.com:443 49.4543 11.0746
relay.dwadziesciajeden.pl 52.2297 21.0122
relay.primal.net 43.6532 -79.3832
nostr2.girino.org:443 43.6532 -79.3832
relay.getvia.xyz 60.1699 24.9384
relay.degmods.com 50.4754 12.3683
relay.edufeed.org 49.4521 11.0767
relay.ltgnet.work:8443 51.0511 -114.075
chorus.mikedilger.com:444 -36.8906 174.794
2 bitchat.nostr1.com 40.7057 -74.0136
nostrrelay.taylorperron.com 45.5029 -73.5723
temp.iris.to 43.6532 -79.3832
relay.ditto.pub 43.6532 -79.3832
nostr-relay.zimage.com 34.282 -118.439
prl.plus 54.9486 21.7283
relay.satsmarkt.club 52.3676 4.90414
nostr.azzamo.net 52.2633 21.0283
nostrelay.circum.space:443 52.2245 8.826
relay.nostrhub.fr 48.1045 11.6004
relay.lightning.pub 39.0438 -77.4874
relay.artx.market 43.6548 -79.3885
nostr.jcera.eu 43.6532 -79.3832
nostr.mom 50.4754 12.3683
relay.plebdevs.com 40.7128 -74.006
nostr-dev.wellorder.net 45.5201 -122.99
3 relay.fundstr.me 42.3601 -71.0589
4 21milionidinostr.duckdns.org nostr.2b9t.xyz 45.519 34.0549 9.08615 -118.243
5 nostr.alex71btc.com armada.sharegap.net 43.6532 -79.3832
6 chat-relay.zap-work.com:443 nostr.chaima.info 43.6532 51.5072 -79.3832 -0.127586
relay.qstr.app 50.1109 8.68213
nostr.middling.mydns.jp 35.8099 140.12
relayone.geektank.ai:443 39.0997 -94.5786
relay.shychat.lol 39.8965 -80.1792
relay.novospes.com 43.6532 -79.3832
relay.bitos.space 43.6532 -79.3832
nostr.rtvslawenia.com 49.4543 11.0746
relay.twentyone.ist 43.7696 -79.3309
relay2.nostrasia.net 43.6532 -79.3832
relay-dev.satlantis.io 39.0438 -77.4874
testnet.samt.st 43.6532 -79.3832
nostr-relay.corb.net 39.6478 -104.988
relay.bullishbounty.com 43.6532 -79.3832
relay.nostr.space 43.6532 -79.3832
relay-dev.gulugulu.moe 43.6532 -79.3832
relay.illuminodes.com 43.6532 -79.3832
nos.lol 50.4754 12.3683
nostr.trevena.info 43.6532 -79.3832
support.flotilla.social 32.9483 -96.7299
no.str.cr:443 8.96171 -83.5246
relay.satlantis.io 39.0438 -77.4874
nostrelay.circum.space 52.2245 8.826
nostr.tbxnetworx.de 47.8267 10.0205
vault.iris.to 43.6532 -79.3832
relay.hazeem.xyz 43.6532 -79.3832
n.kojira.io 43.6532 -79.3832
relay.kilombino.com 43.6532 -79.3832
relay.nostr.net 43.6532 -79.3832
relay.nostrasia.net 43.6532 -79.3832
buzz.cashu.space 43.6532 -79.3832
greenart7c3.feeds.relay.tools 38.6327 -90.1961
nostr.spaceshell.xyz 43.6532 -79.3832
relay.nostriot.com 41.5695 -83.9786
relay.snort.social 53.3498 -6.26031
relay.nostrcade.com 37.79 -122.401
groups.beginningend.com 35.2227 -97.4786
nostr.self-determined.de 51.4379 14.2355
relay.aarpia.com 37.3986 -121.964
relay.underorion.se 51.4998 -0.011072
relay.staging.plebeian.market:443 51.5072 -0.127586
relay.chorus.community 50.1109 8.68213
dm-test-nostr-rs-42-disabled.samt.st 43.6532 -79.3832
relay.staging.plebeian.market 51.5072 -0.127586
relay.snotr.nl:49999 51.9758 4.31389
baunilha.ie.apps.lnvps.cloud 53.3498 -6.26031
n.bogs.ccwu.cc 43.6532 -79.3832
relay.angor.io 48.1046 11.6002
7 nosflare-leefcore.leefcore.workers.dev 43.6532 -79.3832
8 relay1.privkey.io ribo.eu.nostria.app:443 25.7617 43.6532 -80.1918 -79.3832
9 yabu.me relay.lightning.pub 35.6096 39.0438 139.73 -77.4874
10 relay.samt.st relay.nostu.be 40.8302 40.4167 -74.1299 -3.70329
11 nostr.whitenode45.ddns.net 40.55 -74.4758
12 nostr.carroarmato0.be:443 50.914 3.21378
13 cdn.satellite.earth 40.8302 -74.1299
nos.lol:443 50.4754 12.3683
relay.nostrian-conquest.com 41.223 -111.974
vault.iris.to:443 43.6532 -79.3832
relay.orangem.art 43.6532 -79.3832
relay.satmaxt.xyz 43.6532 -79.3832
relay.mwaters.net 50.9871 2.12554
relayrs.notoshi.win:443 43.6532 -79.3832
relay-na1.metanomalist.com 43.6532 -79.3832
relay.piazza.today 48.122 11.589
relay.wavlake.com 41.2619 -95.8608
nostr-relay.xbytez.io:443 50.6924 3.20113
relay.nostrverse.net 43.6532 -79.3832
nostr.malin.onl 43.6532 -79.3832
budabit.nostr1.com 40.7057 -74.0136
bridge.tagomago.me 42.3601 -71.0589
relay.bullishbounty.com:443 43.6532 -79.3832
wheat.oslim.dev 43.6532 -79.3832
nostr.planix.org 43.6532 -79.3832
nostr.zoracle.org 45.6018 -121.185
spurline.safebox.dev 43.6532 -79.3832
relay.chorus.community:443 50.1109 8.68213
nostr.wecsats.io 43.6532 -79.3832
relay.oranjkid.com 41.8781 -87.6298
offchain.bostr.online 43.6532 -79.3832
relay.wavlake.com:443 41.2619 -95.8608
nostr.trepechov.com 49.4543 11.0746
relay.paulstephenborile.com 49.4543 11.0746
14 relay2.veganostr.com 60.1699 24.9384
15 relay.beginningend.com relay.layer.systems:443 35.2227 49.0291 -97.4786 8.35695
16 nostr.data.haus relay0.gfcom.info 50.4754 13.7653 12.3683 100.647
17 bitcoiner.social relay.mmwaves.de:443 47.6743 48.8575 -117.112 2.35138
18 relay.jmoose.rocks:443 offchain.pub 32.7767 39.1585 -96.797 -94.5728
19 strfry.shock.network bcast.girino.org 39.0438 43.6532 -77.4874 -79.3832
20 relay.minibolt.info staging.yabu.me 43.6532 35.6092 -79.3832 139.73
21 relay.nostrfeed.com nostr.overpay.com 60.1699 29.7449 24.9384 -95.5343
22 bridge.tagomago.me 42.3601 -71.0589
23 nostr-01.yakihonne.com 1.32123 103.695
24 strfry.bonsai.com 39.0438 -77.4874
relay2.0xprivacy.online 43.6532 -79.3832
relay.conduit.market 38.7946 -106.535
nostr.pbfs.io 50.4754 12.3683
relay.noeudlibre.fr 50.6924 3.20113
relay.earthly.city 34.1749 -118.54
relay.veganostr.com 60.1699 24.9384
relay.cypherguide.org 1.32123 103.695
relay.routstr.com 59.4016 17.9455
relay.nostrnostr.net 43.6532 -79.3832
no.str.cr 8.96171 -83.5246
relay.littlebitstudios.com 34.8688 -86.5502
relay.decentnewsroom.com 50.4754 12.3683
zencrown.relay2aeon.quest 52.3061 4.6907
cs-relay.nostrdev.com:443 50.4754 12.3683
relay.nostrcheck.me 43.6532 -79.3832
nostr-relayflare.enpitsulin.workers.dev 43.6532 -79.3832
relay.jcera.eu 43.6532 -79.3832
soloco.nl 43.6532 -79.3832
nostr.linky.fit 50.1109 8.68213
relay.nostrops.com 32.71 -96.6745
nostr.nestoris.online 55.568 37.5853
cache.trustr.ing 43.6548 -79.3885
denimroad.feeds.relay.tools 38.6327 -90.1961
nostr.easycryptosend.it 43.6532 -79.3832
nostr.overmind.lol 43.6532 -79.3832
relay.lacrypta.ar 43.6532 -79.3832
torly.smesh.lol 32.7767 -96.797
nostr.wild-vibes.ts.net 48.8566 2.35222
nostr.christiansass.de 51.1798 7.1925
nostr.eu.freedomrelay.dev 49.0524 14.4347
ec1.f7z.io 60.1699 24.9384
relay.selfrule.co.za 43.6532 -79.3832
bbw-nostr.xyz 34.0549 -118.243
slick.mjex.me 39.0418 -77.4744
0x-nostr-relay.fly.dev 38.7946 -106.535
bucket.coracle.social 37.7775 -122.397
relay.44billion.net 43.6532 -79.3832
relayrs.notoshi.win 43.6532 -79.3832
nostr-01.uid.ovh 50.9871 2.12554
strfry.bonsai.com:443 39.0438 -77.4874
porchlight.social 43.6532 -79.3832
fanfares.nostr1.com 40.7057 -74.0136
borgar.lol 52.3676 4.90414
nostr.bitcoiner.social:443 47.6743 -117.112
nostr.bitcoiner.social 47.6743 -117.112
nostr.chaima.info:443 50.1109 8.68213
nostr-relay.corb.net:443 39.6478 -104.988
relay-can.zombi.cloudrodion.com 43.6532 -79.3832
nos.xmark.cc 50.6916 3.20151
relay02.lnfi.network 35.6764 139.65
talon.quest 43.6532 -79.3832
relay.substation.ninja 43.6532 -79.3832
nostr-rs-relay.dev.fedibtc.com:443 39.0438 -77.4874
25 relay.sharegap.net 43.6532 -79.3832
26 nostr.now nostr.islandarea.net 35.8099 35.4669 140.12 -97.6473
27 relayone.geektank.ai dm-test-strfry-generic.samt.st 39.0997 43.6532 -94.5786 -79.3832
28 relay.nostr.black treuzkas.branruz.com 43.6532 48.8575 -79.3832 2.35138
29 relay.cyberguy.fyi relay-rpi.edufeed.org:443 52.3676 49.4521 4.90414 11.0767
30 relay.wisp.talk vault.iris.to:443 49.4543 43.6532 11.0746 -79.3832
relay.mostro.network 40.8302 -74.1299
relay.cloistr.xyz 42.337 -77.3178
chat-relay.zap-work.com 43.6532 -79.3832
relay.wasabiwallet.io 43.6532 -79.3832
relay.atsocy.com 43.6532 -79.3832
articles.layer3.news 37.3387 -121.885
scores.brainstorm.world 39.0997 -94.5786
relay.rebelbase.social 50.965 5.50082
purplerelay.com 43.6532 -79.3832
relay.pprgb.app 43.6532 -79.3832
relay.nostrmap.net 60.1699 24.9384
ynostr.yael.at 60.1699 24.9384
nostr.mom:443 50.4754 12.3683
vidar.nestoris.online 52.3676 4.90414
nostr.trcjr.xyz 35.3821 -78.5486
relay.directsponsor.net 42.8864 -78.8784
relay.duck1123.com 43.6532 -79.3832
relay-dev.gulugulu.moe:443 43.6532 -79.3832
relay.i9.eti.br 43.6532 -79.3832
nostriches.club 43.6532 -79.3832
relay.loveisbitcoin.com 43.6532 -79.3832
relay.openspecs.uid.ovh 50.9871 2.12554
relay.getsafebox.app 43.6532 -79.3832
relay.lanacoin-eternity.com 40.8302 -74.1299
nostr.davenov.com 50.1109 8.68213
relay.tapestry.ninja 40.8054 -74.0241
nostr2.girino.org 43.6532 -79.3832
relay.zone667.com 60.1699 24.9384
nostr.rtvslawenia.com:443 49.4543 11.0746
relay.mccormick.cx 52.3563 4.95714
nostr-relay.cbrx.io 43.6532 -79.3832
nip85.nosfabrica.com 39.0997 -94.5786
dev-relay.nostr.space 43.6532 -79.3832
spamspamspamspam.rest 43.6532 -79.3832
relay.gorrdy.cz 50.1109 8.68213
nostr.bitcoinx.gr 49.0291 8.35696
nostr.sovereignservices.xyz 43.6532 -79.3832
relay.wisp.talk:443 49.4543 11.0746
top.testrelay.top 43.6532 -79.3832
relay.jmoose.rocks 32.7767 -96.797
nostr.relay.hedwig.sh 60.1699 24.9384
adre.su 59.9311 30.3609
nostr.tabordalab.com 60.1699 24.9384
relay.nostrconnect.com 41.5695 -83.9786
relay.hjkl.zip 43.6532 -79.3832
freelay.sovbit.host 60.1699 24.9384
syb.lol 43.6532 -79.3832
relay.nearhood.co.uk 51.5134 -0.0890675
nostr.snowbla.de:443 50.4754 12.3683
relay.getsafebox.app:443 43.6532 -79.3832
relay.inforsupports.com 43.6532 -79.3832
nostr-relay.xbytez.io 50.6924 3.20113
fanfares.nostr1.com:443 40.7057 -74.0136
31 node.kommonzenze.de 49.4521 11.0767
32 nostr.stakey.net:443 nostr.thalheim.io:443 52.6485 60.1699 4.73787 24.9384
33 soloco.nl 43.6532 -79.3832
34 strfry.shock.network 39.0438 -77.4874
35 nostr-relay.zimage.com 34.0549 -118.243
36 public.crostr.com:443 43.6532 -79.3832
37 nostr.sathoarder.com:443 48.5734 7.75211
38 relay.angor.io 48.1046 11.6002
39 relay.wellorder.net 45.5201 -122.99
40 relay.mwaters.net 50.9871 2.12554
41 relay.staging.commonshub.brussels 49.4543 11.0746
42 nostr-verified.wellorder.net 45.5201 -122.99
43 nostr-pub.wellorder.net 45.5201 -122.99
44 nostr-2.21crypto.ch 47.5356 8.73209
45 relay.kaleidoswap.com 50.8476 4.35717
46 relay.libernet.app:443 43.6532 -79.3832
47 relay.homeinhk.xyz 35.694 139.754
48 relay.manneken.brussels 49.4543 11.0746
49 nostr.spicyz.io:443 43.6532 -79.3832
50 relay.lanacoin-eternity.com:443 40.8302 -74.1299
51 ribo.us.nostria.app:443 43.6532 -79.3832
52 relay.loveisbitcoin.com 43.6532 -79.3832
53 relay.angor.io:443 48.1046 11.6002
54 relay02.lnfi.network 35.6764 139.65
55 relay.cosmicbolt.net:443 37.3986 -121.964
56 nostr-rs-relay-qj1h.onrender.com 37.7775 -122.397
57 nrs-01.darkcloudarcade.com 39.0997 -94.5786
58 relay.endfiat.money:443 59.3327 18.0656
59 relay.paulstephenborile.com 49.4543 11.0746
60 rele.speyhard.fi 51.5072 -0.127586
61 relay.froth.zone 60.1699 24.9384
62 relay.nostr.blockhenge.com 39.0438 -77.4874
63 nrl.ceskar.xyz 50.5145 16.0119
64 rilo.nostria.app 43.6532 -79.3832
65 nostr.overmind.lol:443 43.6532 -79.3832
66 nostr.snowbla.de:443 50.4754 12.3683
67 nostrrelay.taylorperron.com 45.5029 -73.5723
68 chorus.pjv.me 45.5201 -122.99
69 relay.nostr.place 43.6532 -79.3832
70 bucket.coracle.social 37.7775 -122.397
71 nostr.girino.org:443 43.6532 -79.3832
72 relay.aarpia.com 37.3986 -121.964
73 nostr.thalheim.io 60.1699 24.9384
74 ec2.f7z.io 60.1699 24.9384
75 relay.trotters.cc 43.6532 -79.3832
76 relay.mccormick.cx:443 52.3563 4.95714
77 relay.momostr.pink 43.6532 -79.3832
78 relay.nostr.net 43.6532 -79.3832
79 conduitl2.fly.dev 37.7648 -122.432
80 chat-relay.zap-work.com 43.6532 -79.3832
81 relay.ditto.pub 43.6532 -79.3832
82 relay.veganostr.com 60.1699 24.9384
83 relay.minibolt.info:443 43.6532 -79.3832
84 adre.su 59.9311 30.3609
85 bitcoinostr.duckdns.org 41.1976 1.11167
86 nostr.computingcache.com:443 34.0356 -118.442
87 relay-fra.zombi.cloudrodion.com 48.8566 2.35222
88 nostr.hekster.org:443 37.3986 -121.964
89 nostr.88mph.life 52.1941 -2.21905
90 wot.dergigi.com 64.1476 -21.9392
91 nostr.planix.org 43.6532 -79.3832
92 relay.satsmarkt.club 52.6907 4.8181
93 nostrcity-club.fly.dev:443 37.7648 -122.432
94 aeon.libretechsystems.xyz 55.486 9.86577
95 testnet.samt.st 43.6532 -79.3832
96 nostr.data.haus 50.4754 12.3683
97 wot.sudocarlos.com 43.6532 -79.3832
98 relay-fra.zombi.cloudrodion.com:443 48.8566 2.35222
99 shu01.shugur.net 21.4902 39.2246
100 relay.gulugulu.moe:443 43.6532 -79.3832
101 relay2.angor.io:443 48.1046 11.6002
102 relay.libernet.app 43.6532 -79.3832
103 directories-safe-motherboard-recipients.trycloudflare.com 43.6532 -79.3832
104 wot.nostr.party 36.1659 -86.7844
105 relay.zone667.com 60.1699 24.9384
106 nostr.wild-vibes.ts.net 48.8566 2.35222
107 relay.nostr.com 50.1109 8.68213
108 nostr.iskarion.ddns.net 43.3076 -2.95421
109 relay-dev.satlantis.io 39.0438 -77.4874
110 relay.sovereignresonance.org 48.9006 2.25929
111 relay.nostrian-conquest.com 41.223 -111.974
112 relay.aidatanorge.no 43.6532 -79.3832
113 strfry.apps3.slidestr.net 40.4167 -3.70329
114 relay.klabo.world 47.2343 -119.853
115 nostr.data.haus:443 50.4754 12.3683
116 testr.nymble.world 40.8054 -74.0241
117 relay.inforsupports.com 43.6532 -79.3832
118 relay.nostrmap.net:443 60.1699 24.9384
119 nostr.stakey.net:443 52.3676 4.90414
120 dev-relay.nostreon.com 60.1699 24.9384
121 nostr.islandarea.net:443 35.4669 -97.6473
122 nostr.rtvslawenia.com 49.4543 11.0746
123 relay.bowlafterbowl.com 32.9483 -96.7299
124 nostr.quali.chat:443 60.1699 24.9384
125 relay.plebeian.market 50.1109 8.68213
126 relay-rpi.edufeed.org 49.4521 11.0767
127 r.0kb.io 32.789 -96.7989
128 nostr.notribe.net:443 40.8302 -74.1299
129 relay.getsafebox.app:443 43.6532 -79.3832
130 nostr.dlcdevkit.com:443 40.0992 -83.1141
131 nostrelites.org 34.9582 -81.9907
132 nostr.hoppe-relay.it.com 42.8864 -78.8784
133 nostr.thebiglake.org 32.71 -96.6745
134 nostr-kyomu-haskell.onrender.com 37.7775 -122.397
135 relay.nostriot.com 41.5695 -83.9786
136 nostr.christiansass.de 51.7634 7.8887
137 relay.btcforplebs.com 43.6532 -79.3832
138 nostr.tagomago.me 42.3601 -71.0589
139 relayone.geektank.ai 39.0997 -94.5786
140 relay.dreamith.to:443 43.6532 -79.3832
141 nostr.liberty.fans 36.8767 -89.5879
142 wot.makenomistakes.ca 43.7064 -79.3986
143 relay.goodmorningbitcoin.com 43.6532 -79.3832
144 relay.layer.systems 49.0291 8.35695
145 relay.paulstephenborile.com:443 49.4543 11.0746
146 relay.ohstr.com 43.6532 -79.3832
147 nostr-relay.xbytez.io:443 50.6924 3.20113
148 nostr.ac 38.958 -77.3592
149 ribo.us.nostria.app 43.6532 -79.3832
150 nostr.21crypto.ch 47.5356 8.73209
151 relay.chorus.community:443 48.5333 10.7
152 relay.cypherflow.ai 48.8575 2.35138
153 relay.agorist.space:443 52.3734 4.89406
154 relay.nostrian-conquest.com:443 41.223 -111.974
155 relay.keykeeper.world 40.7824 -74.0711
156 relay.getvia.xyz 60.1699 24.9384
157 relay.nuts.cash 52.3676 4.90414
158 kotukonostr.onrender.com 37.7775 -122.397
159 relay.minibolt.info 43.6532 -79.3832
160 relay.dwadziesciajeden.pl 52.2297 21.0122
161 relay.fountain.fm:443 43.6532 -79.3832
162 relay.fountain.fm 43.6532 -79.3832
163 nostr-02.uid.ovh 50.9871 2.12554
164 relay.lanavault.space:443 60.1699 24.9384
165 nostr.carroarmato0.be 50.914 3.21378
166 nexus.libernet.app:443 43.6532 -79.3832
167 relay.artio.inf.unibe.ch 46.9501 7.43678
168 blossom.gnostr.cloud 43.6532 -79.3832
169 relay.binaryrobot.com 43.6532 -79.3832
170 relay.earthly.city 34.1749 -118.54
171 nostr.hifish.org 47.4244 8.57658
172 offchain.pub:443 39.1585 -94.5728
173 relay.bullishbounty.com:443 43.6532 -79.3832
174 strfry.openhoofd.nl:443 51.5717 3.70417
175 cs-relay.nostrdev.com:443 50.4754 12.3683
176 strfry.ymir.cloud 43.6532 -79.3832
177 nostrbtc.com 43.6532 -79.3832
178 relay.directsponsor.net 42.8864 -78.8784
179 nostr2.girino.org 43.6532 -79.3832
180 relay.sigit.io:443 50.4754 12.3683
181 relay.getsafebox.app 43.6532 -79.3832
182 antiprimal.net 43.6532 -79.3832
183 nostr.sathoarder.com 48.5734 7.75211
184 inbox.scuba323.com 40.8218 -74.45
185 nrs-01.darkcloudarcade.com:443 39.0997 -94.5786
186 nostr.tac.lol 47.4748 -122.273
187 nostr.davenov.com 50.1109 8.68213
188 relay.trotters.cc:443 43.6532 -79.3832
189 nostr.plantroon.com:443 50.1013 8.62643
190 relay.nostreon.com 60.1699 24.9384
191 nostr.easycryptosend.it 43.6532 -79.3832
192 nostr-01.yakihonne.com:443 1.32123 103.695
193 relay-testnet.k8s.layer3.news 37.3387 -121.885
194 nostr.purpura.cloud 43.6532 -79.3832
195 insta-relay.apps3.slidestr.net 40.4167 -3.70329
196 nostr.mifen.me 43.6532 -79.3832
197 testnet-relay.samt.st:443 40.8302 -74.1299
198 nostr.2b9t.xyz:443 34.0549 -118.243
199 relay.wavlake.com:443 41.2619 -95.8608
200 relay.wisp.talk:443 49.4543 11.0746
201 relay-dev.satlantis.io:443 39.0438 -77.4874
202 relay.satlantis.io 39.0438 -77.4874
203 relay.staging.plebeian.market 51.5072 -0.127586
204 relay.openfarmtools.org 60.1699 24.9384
205 relay.nostrhub.fr 48.1045 11.6004
206 nostr-relay.xbytez.io 50.6924 3.20113
207 relay.binaryrobot.com:443 43.6532 -79.3832
208 relay.samt.st 40.8302 -74.1299
209 relay.illuminodes.com 43.6532 -79.3832
210 relay.liberbitworld.org 43.6532 -79.3832
211 relay.olas.app:443 60.1699 24.9384
212 no.str.cr 8.96171 -83.5246
213 dm-test-strfry-discovery.samt.st 43.6532 -79.3832
214 wot.rejecttheframe.xyz 43.6532 -79.3832
215 relay.nostriot.com:443 41.5695 -83.9786
216 nostr.plantroon.com 50.1013 8.62643
217 nostr-01.uid.ovh 50.9871 2.12554
218 relay.openresist.com:443 43.6532 -79.3832
219 nostr.overmind.lol 43.6532 -79.3832
220 relay.internationalright-wing.org -22.5022 -48.7114
221 nostr.myshosholoza.co.za:443 52.3676 4.90414
222 nostr.pbfs.io:443 50.4754 12.3683
223 21milionidinostr.duckdns.org 41.8967 12.4822
224 nostr.4rs.nl 49.0291 8.35696
225 relay.lanavault.space 60.1699 24.9384
226 relay.mostr.pub 43.6532 -79.3832
227 relay.nostar.org 43.6532 -79.3832
228 nostr.mom 50.4754 12.3683
229 relay.decentralia.fr 48.122 11.589
230 nostrcheck.me relay.agentry.com 43.6532 42.8864 -79.3832 -78.8784
231 relay2.angor.io 48.1046 11.6002
232 slick.mjex.me 39.0418 -77.4744
233 relay-us.zombi.cloudrodion.com 40.7862 -74.0743
234 relay.vrtmrz.net:443 43.6532 -79.3832
235 relay.beginningend.com 35.2227 -97.4786
236 chat-relay.zap-work.com:443 43.6532 -79.3832
237 relay.underorion.se 50.1109 8.68213
238 relay.mitchelltribe.com 39.0438 -77.4874
239 relay.qstr.app 51.5072 -0.127586
240 relay.cyberguy.fyi 52.6907 4.8181
241 strfry.bonsai.com:443 39.0438 -77.4874
242 relayone.soundhsa.com:443 39.0997 -94.5786
243 relay.sigit.io 50.4754 12.3683
244 relay.npubhaus.com 43.6532 -79.3832
245 relayrs.notoshi.win 43.6532 -79.3832
246 relay.mitchelltribe.com:443 39.0438 -77.4874
247 relay.44billion.net 43.6532 -79.3832
248 reraw.pbla2fish.cc 43.6532 -79.3832
249 articles.layer3.news:443 37.3387 -121.885
250 nostr.sovereignservices.xyz 43.6532 -79.3832
251 relay.nostx.io 43.6532 -79.3832
252 nostr-relay.amethyst.name 39.0067 -77.4291
253 0x-nostr-relay.fly.dev 37.7648 -122.432
254 relay.ohstr.com:443 43.6532 -79.3832
255 00f2e774.relay.dev.thunderegg.us 39.0438 -77.4874
256 nostr-relay.cbrx.io 43.6532 -79.3832
257 relay.wavlake.com 41.2619 -95.8608
258 purplerelay.com:443 43.6532 -79.3832
259 nostr-pr02.redscrypt.org 52.3676 4.90414
260 fanfares.nostr1.com:443 40.7057 -74.0136
261 kasztanowa.bieda.it 43.6532 -79.3832
262 relay.flashapp.me 43.6548 -79.3885
263 relay.typedcypher.com 51.5072 -0.127586
264 nostr.bond 50.1109 8.68213
265 nostr.azzamo.net 52.2633 21.0283
266 nexus.libernet.app 43.6532 -79.3832
267 relay.cosmicbolt.net 37.3986 -121.964
268 schnorr.me 43.6532 -79.3832
269 relay.mostro.network:443 40.8302 -74.1299
270 relay-arg.zombi.cloudrodion.com 1.35208 103.82
271 relay.chorus.community 48.5333 10.7
272 blossom.gnostr.cloud:443 43.6532 -79.3832
273 syb.lol:443 34.0549 -118.243
274 relay.dyne.org 49.0291 8.35705
275 btc.klendazu.com 41.2861 1.24993
276 wot.nostr.place 43.6532 -79.3832
277 relay.openresist.com 43.6532 -79.3832
278 rilo.nostria.app:443 43.6532 -79.3832
279 no.str.cr:443 8.96171 -83.5246
280 relay.mostr.pub:443 43.6532 -79.3832
281 relay.edufeed.org:443 49.4521 11.0767
282 nostr.debate.report 50.1109 8.68213
283 relay.satmaxt.xyz:443 43.6532 -79.3832
284 relay.artx.market:443 43.6548 -79.3885
285 relay-dev.gulugulu.moe 43.6532 -79.3832
286 relay.novospes.com 43.6532 -79.3832
287 relay.nostr-check.me 43.6532 -79.3832
288 nostr.computingcache.com 34.0356 -118.442
289 nostr.oxtr.dev 50.4754 12.3683
290 relay.fckstate.net 59.3293 18.0686
291 relay.vrtmrz.net 43.6532 -79.3832
292 relay.bornheimer.app 51.5072 -0.127586
293 relay.guggero.org 46.5971 9.59652
294 relay01.lnfi.network 35.6764 139.65
295 wot.shaving.kiwi 43.6532 -79.3832
296 nostr.twinkle.lol 51.902 7.6657
297 relay.edufeed.org 49.4521 11.0767
298 relay.lanacoin-eternity.com 40.8302 -74.1299
299 relay.satmaxt.xyz 43.6532 -79.3832
300 nostr.hifish.org:443 47.4244 8.57658
301 relay.cypherflow.ai:443 48.8575 2.35138
302 infinity-signal-relay.digitalforlifeagency.workers.dev 43.6532 -79.3832
303 nostr.na.social:443 43.6532 -79.3832
304 nostr.rtvslawenia.com:443 49.4543 11.0746
305 relay.mypathtofire.de 42.8864 -78.8784
306 public.crostr.com 43.6532 -79.3832
307 relay.olas.app 60.1699 24.9384
308 relay.agora.social 50.7383 15.0648
309 ribo.nostria.app 43.6532 -79.3832
310 relay.lab.rytswd.com 49.4543 11.0746
311 relay.ditto.pub:443 43.6532 -79.3832
312 porchlight.social 43.6532 -79.3832
313 nostr.notribe.net 40.8302 -74.1299
314 relay.endfiat.money 59.3327 18.0656
315 nostr.myshosholoza.co.za 52.3676 4.90414
316 relay.nearhood.co.uk 51.5134 -0.0890675
317 relay.degmods.com 50.4754 12.3683
318 nostr.novacisko.cz 52.2026 20.9397
319 prl.plus 55.7628 37.5983
320 bruh.samt.st 43.6532 -79.3832
321 strfry.openhoofd.nl 51.5717 3.70417
322 nostr.spicyz.io 43.6532 -79.3832
323 nostr.na.social 43.6532 -79.3832
324 nip85.nosfabrica.com 39.0997 -94.5786
325 premium.primal.net 43.6532 -79.3832
326 fanfares.nostr1.com 40.7057 -74.0136
327 relay.scuba323.com 40.8218 -74.45
328 nostr2.girino.org:443 43.6532 -79.3832
329 relay.mmwaves.de 48.8575 2.35138
330 nostr-rs-relay.dev.fedibtc.com:443 39.0438 -77.4874
331 strfry.shock.network:443 39.0438 -77.4874
332 nostr.snowbla.de 50.4754 12.3683
333 nostr.spaceshell.xyz 43.6532 -79.3832
334 nostr.quali.chat 60.1699 24.9384
335 wot.utxo.one 43.6532 -79.3832
336 relay.mccormick.cx 52.3563 4.95714
337 mostro-p2p.tech 50.1109 8.68213
338 basspistol.org 49.0291 8.35696
339 ribo.nostria.app:443 43.6532 -79.3832
340 chorus.mikedilger.com:444 -36.8906 174.794
341 nostr.oxtr.dev:443 50.4754 12.3683
342 nostr.nodesmap.com 59.3327 18.0656
343 offchain.bostr.online 43.6532 -79.3832
344 purplerelay.com 43.6532 -79.3832
345 relayrs.notoshi.win:443 43.6532 -79.3832
346 relay.wavefunc.live 41.8781 -87.6298
347 relay.dreamith.to 43.6532 -79.3832
348 bendernostur.duckdns.org:8443 50.1109 8.68213
349 relay.nmail.li 50.9871 2.12554
350 nostr-relay.corb.net 39.6478 -104.988
351 relay.staging.plebeian.market:443 51.5072 -0.127586
352 spamspamspamspam.rest 43.6532 -79.3832
353 relay1.gfcom.info 13.9215 100.538
354 schnorr.me:443 43.6532 -79.3832
355 relay.lab.rytswd.com:443 49.4543 11.0746
356 nostr-rs-relay.dev.fedibtc.com 39.0438 -77.4874
357 dm-test-nostr-rs-42-disabled.samt.st 43.6532 -79.3832
358 relay.nostrmap.net 60.1699 24.9384
359 nostr.relay.hedwig.sh 60.1699 24.9384
360 relay.veganostr.com:443 60.1699 24.9384
361 relay.wavefunc.live:443 41.8781 -87.6298
362 nostr.mikoshi.de 52.52 13.405
363 syb.lol 34.0549 -118.243
364 relay1.nostrchat.io 60.1699 24.9384
365 nostr.wecsats.io:443 43.6532 -79.3832
366 nostr.chaima.info:443 51.5072 -0.127586
367 nostr.azzamo.net:443 52.2633 21.0283
368 relay-can.zombi.cloudrodion.com 43.6532 -79.3832
369 nostr.unkn0wn.world 46.8499 9.53287
370 relayone.soundhsa.com 39.0997 -94.5786
371 x.kojira.io 43.6532 -79.3832
372 dm-test-strfry-discovery.samt.st:443 43.6532 -79.3832
373 nostrelay.circum.space 52.6907 4.8181
374 relay.primal.net 43.6532 -79.3832
375 nostr.girino.org 43.6532 -79.3832
376 nostr.pbfs.io 50.4754 12.3683
377 relay.kalcafe.xyz 37.3986 -121.964
378 relay.gulugulu.moe 43.6532 -79.3832
379 top.testrelay.top 43.6532 -79.3832
380 relay.kilombino.com 43.6532 -79.3832
381 nos.lol:443 50.4754 12.3683
382 nos.lol 50.4754 12.3683
383 relay.nostr.place:443 43.6532 -79.3832
384 cache.trustr.ing 43.6548 -79.3885
385 relay.internationalright-wing.org:443 -22.5022 -48.7114
386 relay.laantungir.net -19.4692 -42.5315
387 relay.lightning.pub:443 39.0438 -77.4874
388 nostr.stakey.net 52.3676 4.90414
389 articles.layer3.news 37.3387 -121.885
390 relay.wisp.talk 49.4543 11.0746
391 relay.pyramid.li 47.4093 8.46503
392 relay.typedcypher.com:443 51.5072 -0.127586
393 dev.relay.stream 43.6532 -79.3832
394 relay.bullishbounty.com 43.6532 -79.3832
395 nostr.mom:443 50.4754 12.3683
396 relay.plebeian.market:443 50.1109 8.68213
397 nostr.hekster.org 37.3986 -121.964
398 nostrcity-club.fly.dev 37.7648 -122.432
399 nostr.vulpem.com 49.4543 11.0746
400 relay-dev.gulugulu.moe:443 43.6532 -79.3832
401 weboftrust.libretechsystems.xyz 55.4724 9.87335
402 nostr-relay.corb.net:443 39.6478 -104.988
403 wheat.happytavern.co 43.6532 -79.3832
404 relay.mappingbitcoin.com 43.6532 -79.3832
405 testnet-relay.samt.st 40.8302 -74.1299
406 relay.bitmacro.cloud 43.6532 -79.3832
407 dev.relay.edufeed.org 49.4521 11.0767
408 myvoiceourstory.org 37.3598 -121.981
409 relay.stickeroo.is-cool.dev 37.3387 -121.885
410 relay.agorist.space 52.3734 4.89406
411 freelay.sovbit.host 60.1699 24.9384
412 nostr-dev.wellorder.net 45.5201 -122.99
413 nostr.middling.mydns.jp 35.8099 140.12
414 cs-relay.nostrdev.com 50.4754 12.3683
415 x.kojira.io:443 43.6532 -79.3832
416 nostrelay.circum.space:443 52.6907 4.8181
417 nostr.janx.com 43.6532 -79.3832
418 relay.mrmave.work 43.6532 -79.3832
419 espelho.girino.org 43.6532 -79.3832
420 hol.is 43.6532 -79.3832
421 ribo.eu.nostria.app 43.6532 -79.3832
422 nostr.yutakobayashi.com 43.6532 -79.3832
423 relay.mostro.network 40.8302 -74.1299
424 communities.nos.social 40.8302 -74.1299
425 relay.solife.me 43.6532 -79.3832
426 yabu.me 35.6092 139.73
427 relay.islandbitcoin.com 12.8498 77.6545
428 nostr.wecsats.io 43.6532 -79.3832
429 nostr.tac.lol:443 47.4748 -122.273
430 relay.arx-ccn.com 50.4754 12.3683
431 nostrride.io 37.3986 -121.964
432 r.0kb.io:443 32.789 -96.7989
433 herbstmeister.com 34.0549 -118.243
434 relay.artx.market 43.6548 -79.3885
435 vault.iris.to 43.6532 -79.3832
436 relay.ru.ac.th 13.7607 100.627
437 temp.iris.to 43.6532 -79.3832
438 social.amanah.eblessing.co 48.1046 11.6002
439 nostr-relay.nextblockvending.com 47.2343 -119.853
440 wot.codingarena.top 50.4754 12.3683
441 relay.sincensura.org 43.6532 -79.3832
442 nostr.dlcdevkit.com 40.0992 -83.1141

View File

@ -3,6 +3,7 @@ package com.bitchat.android.nostr
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.core.content.edit
import java.io.BufferedReader
import java.io.File
import java.io.FileInputStream
@ -26,7 +27,12 @@ import okhttp3.Request
object RelayDirectory {
private const val TAG = "RelayDirectory"
private const val ASSET_FILE_URL = "https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv"
// The source URL matches iOS GeoRelayDirectory. Download timing and parser
// acceptance can leave clients with different loaded snapshots.
internal const val ASSET_FILE_URL = "https://raw.githubusercontent.com/permissionlesstech/bitchat/refs/heads/main/relays/online_relays_gps.csv"
// Download cache of ASSET_FILE_URL above, and the bundled list the weekly job
// refreshes from it; the file names predate the source's move to online_relays_gps.csv.
private const val ASSET_FILE = "nostr_relays.csv"
private const val DOWNLOADED_FILE = "nostr_relays_latest.csv"
private const val PREFS_NAME = "relay_directory_prefs"
@ -49,7 +55,10 @@ object RelayDirectory {
private val relays: MutableList<RelayInfo> = mutableListOf()
private val relaysLock = Any()
fun initialize(application: Application) {
fun initialize(application: Application) =
initialize(application, ioScope, System::currentTimeMillis)
internal fun initialize(application: Application, refreshScope: CoroutineScope, nowMillis: () -> Long) {
if (initialized) return
synchronized(this) {
if (initialized) return
@ -67,15 +76,8 @@ object RelayDirectory {
initialized = true
// Trigger an immediate fetch if the data is stale (older than 24h)
ioScope.launch {
if (isStale(application)) {
fetchAndMaybeSwap(application)
}
}
// Start periodic staleness check every minute
startPeriodicRefresh(application)
// One worker checks immediately, then waits a minute after each iteration.
startPeriodicRefresh(application, refreshScope, nowMillis)
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize RelayDirectory: ${e.message}")
}
@ -97,14 +99,18 @@ object RelayDirectory {
}
val (lat, lon) = center
return snapshot
.asSequence()
.sortedBy { haversineMeters(lat, lon, it.latitude, it.longitude) }
.take(nRelays.coerceAtLeast(0))
.map { it.url }
.toList()
return closestRelays(snapshot, lat, lon, nRelays)
}
// City-level coordinates create distance ties. Break them by endpoint key,
// following GeoRelayDirectory.closestRelays for the accepted directory rows.
internal fun closestRelays(entries: List<RelayInfo>, lat: Double, lon: Double, nRelays: Int): List<String> =
entries
.map { it to haversineMeters(lat, lon, it.latitude, it.longitude) }
.sortedWith(compareBy({ it.second }, { it.first.url }))
.take(nRelays.coerceAtLeast(0))
.map { it.first.url }
private fun haversineMeters(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
val R = 6371000.0 // meters
val dLat = Math.toRadians(lat2 - lat1)
@ -128,18 +134,18 @@ object RelayDirectory {
private fun getDownloadedFile(application: Application): File =
File(application.filesDir, DOWNLOADED_FILE)
private fun isStale(application: Application): Boolean {
private fun isStale(application: Application, nowMillis: () -> Long): Boolean {
val last = getPrefs(application).getLong(KEY_LAST_UPDATE_MS, 0L)
val now = System.currentTimeMillis()
val now = nowMillis()
return now - last >= ONE_DAY_MS
}
private fun startPeriodicRefresh(application: Application) {
ioScope.launch {
private fun startPeriodicRefresh(application: Application, refreshScope: CoroutineScope, nowMillis: () -> Long) {
refreshScope.launch {
while (true) {
try {
if (isStale(application)) {
fetchAndMaybeSwap(application)
if (isStale(application, nowMillis)) {
fetchAndMaybeSwap(application, nowMillis)
}
} catch (e: Exception) {
Log.w(TAG, "Periodic refresh encountered an error: ${e.message}")
@ -149,30 +155,27 @@ object RelayDirectory {
}
}
private fun fetchAndMaybeSwap(application: Application) {
private fun fetchAndMaybeSwap(application: Application, nowMillis: () -> Long) {
var temporaryDownload: File? = null
try {
val tmpFile = File.createTempFile("relays_", ".csv", application.cacheDir)
temporaryDownload = tmpFile
val ok = downloadToFile(ASSET_FILE_URL, tmpFile)
if (!ok) {
Log.w(TAG, "Failed to fetch latest relays; keeping current list (will fallback to bundled if none)")
tmpFile.delete()
Log.w(TAG, "Failed to fetch latest relays; keeping current list")
return
}
val parsed = parseCsv(FileInputStream(tmpFile))
if (parsed.isEmpty()) {
Log.w(TAG, "Downloaded relay CSV parsed to 0 entries; ignoring")
tmpFile.delete()
return
}
val dest = getDownloadedFile(application)
tmpFile.inputStream().use { input ->
FileOutputStream(dest, false).use { output ->
input.copyTo(output)
}
RelayDirectoryCache.replace(input, dest)
}
tmpFile.delete()
val hash = fileSha256Hex(dest)
val entries = parsed.size
@ -182,11 +185,17 @@ object RelayDirectory {
relays.addAll(parsed)
}
getPrefs(application).edit().putLong(KEY_LAST_UPDATE_MS, System.currentTimeMillis()).apply()
getPrefs(application).edit { putLong(KEY_LAST_UPDATE_MS, nowMillis()) }
Log.i(TAG, "✅ Using downloaded relay list (${dest.absolutePath}), entries=$entries, sha256=$hash, updatedAtMs=${getPrefs(application).getLong(KEY_LAST_UPDATE_MS, 0L)}")
} catch (e: Exception) {
Log.w(TAG, "Failed to fetch and swap relay list: ${e.message}")
} finally {
temporaryDownload?.let { file ->
if (file.exists() && !file.delete()) {
Log.w(TAG, "Could not remove temporary relay download")
}
}
}
}
@ -195,10 +204,10 @@ object RelayDirectory {
val req = Request.Builder().url(url).get().build()
httpClient.newCall(req).execute().use { resp ->
if (!resp.isSuccessful) {
Log.w(TAG, "HTTP ${'$'}{resp.code} when fetching $url")
Log.w(TAG, "HTTP ${resp.code} when fetching $url")
return false
}
val body = resp.body ?: return false
val body = resp.body
FileOutputStream(dest).use { out ->
body.byteStream().use { input ->
input.copyTo(out)
@ -250,13 +259,17 @@ object RelayDirectory {
streamSha256Hex(input)
}
} catch (e: Exception) {
"error:${'$'}{e.message}"
"error:${e.message}"
}
Log.i(TAG, "📦 Loaded ${list.size} relay entries from assets/$ASSET_FILE, sha256=$hash")
}
private fun parseCsv(input: InputStream): List<RelayInfo> {
internal fun parseCsv(input: InputStream): List<RelayInfo> {
val result = mutableListOf<RelayInfo>()
// The directory lists some relays twice, once bare and once with an explicit
// :443, which has the same endpoint key over wss. Without this check both copies can
// land in a nearest-N selection, and one of its slots connects nowhere new.
val seenEndpoints = HashSet<String>()
BufferedReader(InputStreamReader(input)).use { reader ->
var line: String?
while (true) {
@ -267,16 +280,38 @@ object RelayDirectory {
if (trimmed.lowercase().startsWith("relay url")) continue
val parts = trimmed.split(",")
if (parts.size < 3) continue
val url = normalizeRelayUrl(parts[0].trim())
val raw = normalizeRelayUrl(parts[0].trim())
val lat = parts[1].trim().toDoubleOrNull()
val lon = parts[2].trim().toDoubleOrNull()
if (url.isEmpty() || lat == null || lon == null) continue
result.add(RelayInfo(url = url, latitude = lat, longitude = lon))
if (raw.isEmpty() || lat == null || lon == null) continue
val canonical = canonicalHost(raw)
if (canonical.isEmpty() || !seenEndpoints.add(canonical)) continue
result.add(RelayInfo(url = "wss://$canonical", latitude = lat, longitude = lon))
}
}
return result
}
/**
* Endpoint key for deduplication and distance ties: lowercase host, with
* port 443 omitted. This parser does not implement iOS directory validation.
*/
internal fun canonicalHost(url: String): String {
val uri = try { java.net.URI(normalizeRelayUrl(url)) } catch (_: Exception) { return "" }
if (uri.scheme?.lowercase() !in setOf("wss", "https")) return ""
if (uri.rawUserInfo != null || uri.rawQuery != null || uri.rawFragment != null) return ""
if (!uri.rawPath.isNullOrEmpty() && uri.rawPath != "/") return ""
val hostPort = uri.rawAuthority ?: return ""
// Reject URL structure that cannot be represented by a directory endpoint key.
if (!hostPort.matches(Regex("[A-Za-z0-9.-]+(?::[0-9]+)?"))) return ""
val idx = hostPort.lastIndexOf(':')
val hasPort = idx > 0
val host = (if (hasPort) hostPort.substring(0, idx) else hostPort).lowercase()
val port = if (hasPort) (hostPort.substring(idx + 1).toIntOrNull() ?: return "") else 443
if (port !in 1..65535) return ""
return if (port == 443) host else "$host:$port"
}
private fun fileSha256Hex(file: File): String = try {
FileInputStream(file).use { input ->
streamSha256Hex(input)

View File

@ -0,0 +1,19 @@
package com.bitchat.android.nostr
import java.io.File
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.StandardCopyOption.ATOMIC_MOVE
internal object RelayDirectoryCache {
fun replace(input: InputStream, destination: File) {
// Stage beside the destination to keep the rename on the same filesystem.
val staged = File.createTempFile("relays_install_", ".csv", destination.absoluteFile.parentFile)
try {
staged.outputStream().use { output -> input.copyTo(output) }
Files.move(staged.toPath(), destination.toPath(), ATOMIC_MOVE)
} finally {
Files.deleteIfExists(staged.toPath())
}
}
}

View File

@ -0,0 +1,70 @@
package com.bitchat.android.nostr
import java.io.ByteArrayInputStream
import org.junit.Assert.assertEquals
import org.junit.Test
class RelayDirectoryAddressTest {
private fun assertRejectedBetweenValidRows(address: String) {
val csv = "Relay URL,Latitude,Longitude\n" +
"first.example,1,2\n$address,3,4\nlast.example:8443,5,6\n"
val entries = RelayDirectory.parseCsv(ByteArrayInputStream(csv.toByteArray()))
assertEquals(address, listOf("wss://first.example", "wss://last.example:8443"), entries.map { it.url })
assertEquals(listOf(1.0 to 2.0, 5.0 to 6.0), entries.map { it.latitude to it.longitude })
}
@Test
fun `rejects paths and preserves neighboring rows`() {
assertRejectedBetweenValidRows("wss://relay.example/a")
}
@Test
fun `rejects nonsecure websocket schemes and preserves neighboring rows`() {
assertRejectedBetweenValidRows("ws://relay.example:80")
}
@Test
fun `rejects http schemes and preserves neighboring rows`() {
assertRejectedBetweenValidRows("http://relay.example")
}
@Test
fun `rejects userinfo and preserves neighboring rows`() {
assertRejectedBetweenValidRows("wss://user@relay.example")
}
@Test
fun `rejects queries and preserves neighboring rows`() {
assertRejectedBetweenValidRows("wss://relay.example?query=1")
}
@Test
fun `rejects fragments and preserves neighboring rows`() {
assertRejectedBetweenValidRows("wss://relay.example#fragment")
}
@Test
fun `rejects nonnumeric ports and preserves neighboring rows`() {
assertRejectedBetweenValidRows("wss://relay.example:invalid")
}
@Test
fun `rejects zero ports and preserves neighboring rows`() {
assertRejectedBetweenValidRows("wss://relay.example:0")
}
@Test
fun `rejects ports above range and preserves neighboring rows`() {
assertRejectedBetweenValidRows("wss://relay.example:65536")
}
@Test
fun `secure schemes and bare hosts retain endpoint normalization`() {
for (address in listOf("relay.example", "wss://RELAY.example:443/", "https://relay.example/")) {
assertEquals(address, "relay.example", RelayDirectory.canonicalHost(address))
}
for (port in listOf(1, 8443, 65535)) {
assertEquals("relay.example:$port", RelayDirectory.canonicalHost("wss://relay.example:$port/"))
}
}
}

View File

@ -0,0 +1,44 @@
package com.bitchat.android.nostr
import java.io.File
import java.io.IOException
import java.io.InputStream
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class RelayDirectoryCacheTest {
@get:Rule val temporary = TemporaryFolder()
@Test
fun `failed staging keeps the previous cache and removes staging files`() {
val destination = temporary.newFile("directory.csv").apply { writeText("previous") }
val input = object : InputStream() {
var reads = 0
override fun read(): Int = if (reads++ < 4) 'x'.code else throw IOException("synthetic read failure")
}
assertThrows(IOException::class.java) { input.use { RelayDirectoryCache.replace(it, destination) } }
assertEquals("previous", destination.readText())
assertEquals(listOf("directory.csv"), temporary.root.list()!!.sorted())
}
@Test
fun `successful staging replaces the existing cache`() {
val destination = temporary.newFile("directory.csv").apply { writeText("previous") }
"replacement".byteInputStream().use { RelayDirectoryCache.replace(it, destination) }
assertEquals("replacement", destination.readText())
assertEquals(listOf("directory.csv"), temporary.root.list()!!.sorted())
}
@Test
fun `failed rename keeps the destination directory and removes staging files`() {
val destination = temporary.newFolder("directory.csv")
File(destination, "keep").writeText("previous")
assertThrows(IOException::class.java) {
"replacement".byteInputStream().use { RelayDirectoryCache.replace(it, destination) }
}
assertEquals("previous", File(destination, "keep").readText())
assertEquals(listOf("directory.csv"), temporary.root.list()!!.sorted())
}
}

View File

@ -0,0 +1,22 @@
package com.bitchat.android.nostr
import org.junit.Assert.assertEquals
import org.junit.Test
class RelayDirectoryPortTest {
@Test
fun `an oversized numeric port does not discard surrounding valid rows`() {
val csv = "Relay URL,Latitude,Longitude\n" +
"first.example,0,0\n" +
"oversized.example:999999999999999999999,1,2\n" +
"last.example:8443,3,4\n"
val entries = RelayDirectory.parseCsv(csv.byteInputStream())
assertEquals(listOf("wss://first.example", "wss://last.example:8443"), entries.map { it.url })
}
@Test
fun `ordinary numeric ports retain their endpoint keys`() {
assertEquals("relay.example", RelayDirectory.canonicalHost("wss://relay.example:443"))
assertEquals("relay.example:8443", RelayDirectory.canonicalHost("wss://relay.example:8443"))
}
}

View File

@ -0,0 +1,193 @@
package com.bitchat.android.nostr
import android.app.Application
import android.content.Context
import android.content.res.AssetManager
import java.io.IOException
import androidx.test.core.app.ApplicationProvider
import com.bitchat.android.net.OkHttpProvider
import java.io.File
import java.util.concurrent.atomic.AtomicReference
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.job
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowLog
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], manifest = Config.NONE)
class RelayDirectoryRefreshRobolectricTest {
@get:Rule val temporary = TemporaryFolder()
private val application = mock<Application>()
private val context: Application = ApplicationProvider.getApplicationContext()
private val prefs = context.getSharedPreferences("relay-refresh-test", Context.MODE_PRIVATE)
private val scheduler = TestCoroutineScheduler()
private val refreshScope = TestScope(StandardTestDispatcher(scheduler))
private var requests = 0
private val now = 172_800_000L
private lateinit var files: File
private lateinit var cache: File
private val previous = "Relay URL,Latitude,Longitude\nprevious.example,0,0\n"
private val update = previous + "updated.example,0,0\n" +
(1..48).joinToString("\n", postfix = "\n") { "other-$it.example,1,1" }
@Before
fun setUp() {
RelayDirectory::class.java.getDeclaredField("initialized").apply { isAccessible = true }
.setBoolean(RelayDirectory, false)
files = temporary.newFolder("files")
cache = temporary.newFolder("cache")
whenever(application.filesDir).thenReturn(files)
whenever(application.cacheDir).thenReturn(cache)
whenever(application.getSharedPreferences("relay_directory_prefs", Context.MODE_PRIVATE)).thenReturn(prefs)
prefs.edit().clear().putLong("last_update_ms", 123L)
.putString("source_url", RelayDirectory.ASSET_FILE_URL).commit()
val file = File(files, "nostr_relays_latest.csv").apply { writeText(previous) }
RelayDirectory::class.java.getDeclaredMethod("loadFromFile", File::class.java, String::class.java)
.apply { isAccessible = true }.invoke(RelayDirectory, file, "synthetic")
ShadowLog.clear()
}
@After
fun tearDown() {
refreshScope.cancel()
scheduler.runCurrent()
RelayDirectory::class.java.getDeclaredField("initialized").apply { isAccessible = true }
.setBoolean(RelayDirectory, false)
val field = RelayDirectory::class.java.getDeclaredField("relays").apply { isAccessible = true }
(field.get(RelayDirectory) as MutableList<*>).clear()
prefs.edit().clear().commit()
}
private fun withResponse(code: Int, body: String, block: () -> Unit) {
val client = OkHttpClient.Builder().addInterceptor { chain ->
requests++
Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1)
.code(code).message("synthetic").body(body.toResponseBody()).build()
}.build()
val field = OkHttpProvider::class.java.getDeclaredField("httpClientRef").apply { isAccessible = true }
@Suppress("UNCHECKED_CAST")
val reference = field.get(OkHttpProvider) as AtomicReference<OkHttpProvider.RoutedClient?>
val saved = reference.getAndSet(OkHttpProvider.RoutedClient(client, OkHttpProvider.Route.DIRECT))
try {
block()
} finally {
reference.set(saved)
client.dispatcher.executorService.shutdown()
client.connectionPool.evictAll()
}
}
private fun fetch(code: Int, body: String) = withResponse(code, body) {
RelayDirectory::class.java.getDeclaredMethod("fetchAndMaybeSwap", Application::class.java, Function0::class.java)
.apply { isAccessible = true }.invoke(RelayDirectory, application, { now })
}
@Test
fun `stale initialization uses one refresh worker and one request per interval`() {
withResponse(503, "unavailable") {
RelayDirectory.initialize(application, refreshScope) { now }
RelayDirectory.initialize(application, refreshScope) { now }
assertEquals(1, refreshScope.coroutineContext.job.children.count())
scheduler.runCurrent()
assertEquals(1, requests)
scheduler.advanceTimeBy(59_999)
scheduler.runCurrent()
assertEquals(1, requests)
scheduler.advanceTimeBy(1)
scheduler.runCurrent()
assertEquals(2, requests)
}
}
@Test
fun `successful initialization records injected time and refreshes at the day boundary`() {
var clock = now
val day = 86_400_000L
withResponse(200, update) {
RelayDirectory.initialize(application, refreshScope) { clock }
scheduler.runCurrent()
assertEquals(1, requests)
assertEquals(clock, prefs.getLong("last_update_ms", 0L))
clock = now + day - 1
scheduler.advanceTimeBy(60_000)
scheduler.runCurrent()
assertEquals("no refresh before the injected day boundary", 1, requests)
assertEquals(now, prefs.getLong("last_update_ms", 0L))
clock = now + day
scheduler.advanceTimeBy(60_000)
scheduler.runCurrent()
assertEquals("refresh at the injected day boundary", 2, requests)
assertEquals(clock, prefs.getLong("last_update_ms", 0L))
}
}
private fun assertPrevious() {
assertEquals(previous, File(files, "nostr_relays_latest.csv").readText())
assertEquals(listOf("wss://previous.example"), RelayDirectory.closestRelaysForGeohash("s0000", 5))
assertEquals(123L, prefs.getLong("last_update_ms", 0L))
assertTrue(cache.list()!!.isEmpty())
}
@Test
fun `asset hash failure reports the exception text`() {
val assets = mock<AssetManager>()
whenever(application.assets).thenReturn(assets)
whenever(assets.open("nostr_relays.csv")).thenAnswer { throw IOException("synthetic asset failure") }
RelayDirectory::class.java.getDeclaredMethod("loadFromAssets", Application::class.java)
.apply { isAccessible = true }.invoke(RelayDirectory, application)
assertTrue(ShadowLog.getLogsForTag("RelayDirectory").any {
it.msg.contains("sha256=error:synthetic asset failure")
})
}
@Test
fun `HTTP failure retains state cleans download and reports status code`() {
fetch(503, "unavailable")
assertPrevious()
assertTrue(ShadowLog.getLogsForTag("RelayDirectory").any { it.msg.contains("HTTP 503") })
}
@Test
fun `unusable destination retains state and cleans temporary download`() {
whenever(application.filesDir).thenReturn(File(files, "missing-parent"))
fetch(200, update)
assertPrevious()
assertTrue(ShadowLog.getLogsForTag("RelayDirectory").any { it.msg.startsWith("Failed to fetch and swap") })
}
@Test
fun `rejected empty download retains state and cleans temporary download`() {
fetch(200, "")
assertPrevious()
}
@Test
fun `successful refresh installs cache updates selection and timestamp`() {
fetch(200, update)
assertEquals(update, File(files, "nostr_relays_latest.csv").readText())
assertEquals(listOf("wss://previous.example", "wss://updated.example"), RelayDirectory.closestRelaysForGeohash("s0000", 2))
assertEquals(now, prefs.getLong("last_update_ms", 0L))
assertTrue(cache.list()!!.isEmpty())
}
}

View File

@ -0,0 +1,130 @@
package com.bitchat.android.nostr
import java.io.ByteArrayInputStream
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
/** Covers the configured source URL, endpoint deduplication and deterministic distance ties. */
class RelayDirectoryTest {
private fun parse(csv: String) =
RelayDirectory.parseCsv(ByteArrayInputStream(csv.toByteArray()))
private val header = "Relay URL,Latitude,Longitude\n"
@Test
fun `fetch url points to online_relays_gps csv`() {
assertEquals(
"https://raw.githubusercontent.com/permissionlesstech/bitchat/refs/heads/main/relays/online_relays_gps.csv",
RelayDirectory.ASSET_FILE_URL
)
}
@Test
fun `canonical host normalizes host case and default port`() {
assertEquals("relay.example.com", RelayDirectory.canonicalHost("wss://relay.example.com"))
assertEquals("relay.example.com", RelayDirectory.canonicalHost("wss://relay.example.com:443"))
assertEquals("relay.example.com:8443", RelayDirectory.canonicalHost("wss://relay.example.com:8443"))
assertEquals("relay.example.com", RelayDirectory.canonicalHost("wss://Relay.Example.Com/"))
}
@Test
fun `port 443 variant of a host is the same endpoint key and is not listed twice`() {
val entries = parse(
header +
"relay.example.com,10.0,20.0\n" +
"relay.example.com:443,10.0,20.0\n"
)
assertEquals(1, entries.size)
assertEquals("wss://relay.example.com", entries[0].url)
}
@Test
fun `a nonstandard port is a distinct endpoint and stays`() {
// A nondefault port has a distinct endpoint key.
val entries = parse(
header +
"port-variant.relay.example,1.0,1.0\n" +
"port-variant.relay.example:8443,1.0,1.0\n"
)
assertEquals(2, entries.size)
assertEquals("wss://port-variant.relay.example", entries[0].url)
assertEquals("wss://port-variant.relay.example:8443", entries[1].url)
}
@Test
fun `first row wins when an endpoint is listed twice`() {
val entries = parse(
header +
"relay.example.com,10.0,20.0\n" +
"relay.example.com:443,50.0,60.0\n"
)
assertEquals(1, entries.size)
assertEquals(10.0, entries[0].latitude, 0.0)
assertEquals(20.0, entries[0].longitude, 0.0)
}
@Test
fun `host case does not create a second endpoint`() {
val entries = parse(
header +
"Relay.Example.Com:443,10.0,20.0\n" +
"relay.example.com,10.0,20.0\n"
)
assertEquals(1, entries.size)
assertEquals("wss://relay.example.com", entries[0].url)
}
@Test
fun `distance ties order by endpoint key`() {
// Shared coordinates create ties; shuffled rows exercise the endpoint tie-break.
val csv = header +
"delta.example.com,12.34,56.78\n" +
"foxtrot.example.com,12.34,56.78\n" +
"alpha.example.com:443,12.34,56.78\n" +
"echo.example.com,12.34,56.78\n" +
"bravo.example.com,12.34,56.78\n" +
"charlie.example.com,12.34,56.78\n"
val five = RelayDirectory.closestRelays(parse(csv), 12.34, 56.78, 5)
assertEquals(
listOf(
"wss://alpha.example.com",
"wss://bravo.example.com",
"wss://charlie.example.com",
"wss://delta.example.com",
"wss://echo.example.com"
),
five
)
}
@Test
fun `five nearest means five distinct endpoint keys`() {
// The nearest relay is listed twice
// (bare and :443), which used to occupy two of the five selection slots and
// push out the fifth distinct endpoint.
val csv = header +
"nearest.example.com,10.10,20.20\n" +
"nearest.example.com:443,10.10,20.20\n" +
"second.example.com,10.20,20.20\n" +
"third.example.com,10.30,20.20\n" +
"fourth.example.com,10.40,20.20\n" +
"fifth.example.com,10.50,20.20\n" +
"faraway.example.com,80.0,120.0\n"
val five = RelayDirectory.closestRelays(parse(csv), 10.10, 20.20, 5)
assertEquals(5, five.size)
assertEquals("selected endpoint keys are distinct", 5, five.toSet().size)
assertTrue("the fifth distinct endpoint makes the cut", five.contains("wss://fifth.example.com"))
assertEquals(
listOf(
"wss://nearest.example.com",
"wss://second.example.com",
"wss://third.example.com",
"wss://fourth.example.com",
"wss://fifth.example.com"
),
five
)
}
}

View File

@ -0,0 +1,41 @@
package com.bitchat.android.nostr
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
class RelaySelectionManifestTest {
private val header = "id\tcsv\tlatitude\tlongitude\tcount\texpected\n"
private val row = "one\tties.csv\t0\t0\t1\twss://alpha.example\n"
@Test
fun `valid manifest provides a named case`() {
assertEquals("one", RelaySelectionVectorTest.parseCases(header + row).single()[0])
}
@Test
fun `empty manifest is rejected`() {
for (text in listOf("", header)) {
assertThrows(IllegalArgumentException::class.java) { RelaySelectionVectorTest.parseCases(text) }
}
}
@Test
fun `wrong header and duplicate IDs are rejected`() {
for (text in listOf("wrong\n" + row, header + row + row)) {
assertThrows(IllegalArgumentException::class.java) { RelaySelectionVectorTest.parseCases(text) }
}
}
@Test
fun `malformed fields are rejected`() {
for (bad in listOf(
row.replace("ties.csv", "../ties.csv"),
row.replace("\t0\t0\t", "\tNaN\t0\t"),
row.replace("\t1\t", "\t2147483648\t"),
row.replace("wss://alpha.example", "")
)) {
assertThrows(IllegalArgumentException::class.java) { RelaySelectionVectorTest.parseCases(header + bad) }
}
}
}

View File

@ -0,0 +1,47 @@
package com.bitchat.android.nostr
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
/** Shared literal expectations for accepted directory rows, not parser equivalence. */
@RunWith(Parameterized::class)
class RelaySelectionVectorTest(private val id: String, private val fields: List<String>) {
@Test
fun `selection matches the shared vector`() {
val entries = resource(fields[1]).byteInputStream().use { RelayDirectory.parseCsv(it) }
val expected = if (fields[5] == "-") emptyList() else fields[5].split('|')
assertEquals(id, expected, RelayDirectory.closestRelays(
entries, fields[2].toDouble(), fields[3].toDouble(), fields[4].toInt()
))
}
companion object {
private fun resource(name: String): String = requireNotNull(
RelaySelectionVectorTest::class.java.getResourceAsStream("/contracts/relay-selection-v1/$name")
).bufferedReader().use { it.readText() }
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun cases(): List<Array<Any>> = parseCases(resource("cases.tsv"))
internal fun parseCases(text: String): List<Array<Any>> {
val lines = text.lineSequence().filter { it.isNotBlank() }.toList()
require(lines.firstOrNull() == "id\tcsv\tlatitude\tlongitude\tcount\texpected") { "Invalid vector header" }
require(lines.size > 1) { "No selection vectors" }
val ids = mutableSetOf<String>()
return lines.drop(1).map { line ->
val fields = line.split('\t')
require(fields.size == 6) { "Invalid vector fields" }
require(fields[0].isNotBlank() && ids.add(fields[0])) { "Invalid or duplicate vector ID" }
require(Regex("[a-z0-9-]+\\.csv").matches(fields[1])) { "Invalid vector filename" }
require(fields[2].toDouble().isFinite() && fields[2].toDouble() in -90.0..90.0)
require(fields[3].toDouble().isFinite() && fields[3].toDouble() in -180.0..180.0)
fields[4].toInt()
require(fields[5] == "-" || fields[5].split('|').all { it.startsWith("wss://") && it.length > 6 })
arrayOf<Any>(fields[0], fields)
}
}
}
}

View File

@ -0,0 +1,14 @@
id csv latitude longitude count expected
ties-five ties.csv 12.34 56.78 5 wss://alpha.example|wss://bravo.example|wss://charlie.example|wss://delta.example|wss://echo.example
ties-reversed ties-reversed.csv 12.34 56.78 5 wss://alpha.example|wss://bravo.example|wss://charlie.example|wss://delta.example|wss://echo.example
ties-one ties.csv 12.34 56.78 1 wss://alpha.example
ties-all ties.csv 12.34 56.78 10 wss://alpha.example|wss://bravo.example|wss://charlie.example|wss://delta.example|wss://echo.example|wss://foxtrot.example
zero-count ties.csv 12.34 56.78 0 -
negative-count ties.csv 12.34 56.78 -1 -
duplicates-five duplicates.csv 10.1 20.2 5 wss://nearest.example|wss://nearest.example:8443|wss://second.example|wss://third.example|wss://fourth.example
duplicates-all duplicates.csv 10.1 20.2 20 wss://nearest.example|wss://nearest.example:8443|wss://second.example|wss://third.example|wss://fourth.example|wss://far.example
longitude-east longitude.csv 0 19 3 wss://near.example|wss://middle.example|wss://far.example
longitude-west longitude.csv 0 69 3 wss://far.example|wss://middle.example|wss://near.example
dateline-east dateline.csv 0 179 3 wss://near.example|wss://across.example|wss://far.example
dateline-west dateline.csv 0 -179 3 wss://across.example|wss://near.example|wss://far.example
latitude-weight latitude-weight.csv 60 20 3 wss://east.example|wss://north.example|wss://far.example
1 id csv latitude longitude count expected
2 ties-five ties.csv 12.34 56.78 5 wss://alpha.example|wss://bravo.example|wss://charlie.example|wss://delta.example|wss://echo.example
3 ties-reversed ties-reversed.csv 12.34 56.78 5 wss://alpha.example|wss://bravo.example|wss://charlie.example|wss://delta.example|wss://echo.example
4 ties-one ties.csv 12.34 56.78 1 wss://alpha.example
5 ties-all ties.csv 12.34 56.78 10 wss://alpha.example|wss://bravo.example|wss://charlie.example|wss://delta.example|wss://echo.example|wss://foxtrot.example
6 zero-count ties.csv 12.34 56.78 0 -
7 negative-count ties.csv 12.34 56.78 -1 -
8 duplicates-five duplicates.csv 10.1 20.2 5 wss://nearest.example|wss://nearest.example:8443|wss://second.example|wss://third.example|wss://fourth.example
9 duplicates-all duplicates.csv 10.1 20.2 20 wss://nearest.example|wss://nearest.example:8443|wss://second.example|wss://third.example|wss://fourth.example|wss://far.example
10 longitude-east longitude.csv 0 19 3 wss://near.example|wss://middle.example|wss://far.example
11 longitude-west longitude.csv 0 69 3 wss://far.example|wss://middle.example|wss://near.example
12 dateline-east dateline.csv 0 179 3 wss://near.example|wss://across.example|wss://far.example
13 dateline-west dateline.csv 0 -179 3 wss://across.example|wss://near.example|wss://far.example
14 latitude-weight latitude-weight.csv 60 20 3 wss://east.example|wss://north.example|wss://far.example

View File

@ -0,0 +1,4 @@
Relay URL,Latitude,Longitude
far.example,0,150
across.example,0,-179
near.example,0,178
1 Relay URL Latitude Longitude
2 far.example 0 150
3 across.example 0 -179
4 near.example 0 178

View File

@ -0,0 +1,8 @@
Relay URL,Latitude,Longitude
Nearest.Example:443,10.1,20.2
nearest.example,10.1,20.2
nearest.example:8443,10.1,20.2
second.example,10.2,20.2
third.example,10.3,20.2
fourth.example,10.4,20.2
far.example,80,120
1 Relay URL Latitude Longitude
2 Nearest.Example:443 10.1 20.2
3 nearest.example 10.1 20.2
4 nearest.example:8443 10.1 20.2
5 second.example 10.2 20.2
6 third.example 10.3 20.2
7 fourth.example 10.4 20.2
8 far.example 80 120

View File

@ -0,0 +1,4 @@
Relay URL,Latitude,Longitude
east.example,60,23
north.example,62,20
far.example,63,24
1 Relay URL Latitude Longitude
2 east.example 60 23
3 north.example 62 20
4 far.example 63 24

View File

@ -0,0 +1,4 @@
Relay URL,Latitude,Longitude
far.example,0,70
near.example,0,20
middle.example,0,40
1 Relay URL Latitude Longitude
2 far.example 0 70
3 near.example 0 20
4 middle.example 0 40

View File

@ -0,0 +1,7 @@
Relay URL,Latitude,Longitude
charlie.example,12.34,56.78
bravo.example,12.34,56.78
echo.example,12.34,56.78
Alpha.Example:443,12.34,56.78
foxtrot.example,12.34,56.78
delta.example,12.34,56.78
1 Relay URL Latitude Longitude
2 charlie.example 12.34 56.78
3 bravo.example 12.34 56.78
4 echo.example 12.34 56.78
5 Alpha.Example:443 12.34 56.78
6 foxtrot.example 12.34 56.78
7 delta.example 12.34 56.78

View File

@ -0,0 +1,7 @@
Relay URL,Latitude,Longitude
delta.example,12.34,56.78
foxtrot.example,12.34,56.78
Alpha.Example:443,12.34,56.78
echo.example,12.34,56.78
bravo.example,12.34,56.78
charlie.example,12.34,56.78
1 Relay URL Latitude Longitude
2 delta.example 12.34 56.78
3 foxtrot.example 12.34 56.78
4 Alpha.Example:443 12.34 56.78
5 echo.example 12.34 56.78
6 bravo.example 12.34 56.78
7 charlie.example 12.34 56.78

View File

@ -1,25 +1,31 @@
# Client rewrite compatibility contracts
This document defines the behavior a from-scratch BitChat client must preserve.
The executable source of truth is the JVM test suite under
`app/src/test/**/contracts`, together with the pre-existing protocol, security,
mesh, and state tests.
The JVM contract, protocol, security, mesh and state tests provide executable
checks for parts of this contract.
The remaining implementation work and milestone progress are tracked in
[test-implementation-plan.md](test-implementation-plan.md).
## Required contract layers
| Layer | Compatibility promise | Primary tests |
The listed tests cover parts of each layer; they do not verify every requirement
in the corresponding row.
| Layer | Required compatibility behavior | Selected tests (partial coverage) |
|---|---|---|
| Outer mesh packet | v1/v2 header widths, big-endian fields, flags, section order, route placement, signature placement, padding, compression, signing bytes, per-type payload caps ([PACKET_PAYLOAD_LIMITS.md](PACKET_PAYLOAD_LIMITS.md)) | `BinaryProtocolTest`, `ClientRewriteWireContractTest`, `PacketPayloadLimitsWireContractTest` |
| Chat payload | Flag bits, millisecond timestamp, UTF-8 byte lengths, encrypted-content substitution, optional-field order | `ClientRewriteWireContractTest` |
| Inner payloads | Noise type bytes, private-message TLVs, peer-state TLVs, file-transfer TLVs, live-voice bursts, fragment header, sync request TLVs | `ClientRewriteWireContractTest`, `AuthenticatedPeerStateTest`, `PrivateMediaTransferPreparerTest`, `VoiceBurstPacketTest`, `FragmentManagerTest` |
| Identity/security | Announcement extensions, capability bitfield endianness, Noise static-key binding, handshake identity binding, signatures | `IdentityAnnouncementTest`, `NoiseSessionManagerIdentityBindingTest`, `ClientRewritePrimitiveContractTest` |
| Sync/routing | Stable packet IDs, GCS bitstream, replay collapse, TTL handling, relay choice, confirmed graph edges | `ClientRewritePrimitiveContractTest`, `GCSFilterTest`, `PacketRelayManagerTest`, `MeshGraphServiceTest`, `TransportBridgeServiceTest` |
| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals, 22h outbound envelope randomization | `ClientRewriteNostrContractTest`, `NostrProtocolTest` |
| Identity/security | Announcement extensions, capability bitfield endianness, Noise static-key binding, handshake identity binding, signatures | `IdentityAnnouncementTest`, `NoiseSessionManagerIdentityBindingTest` |
| Sync/routing | Stable packet IDs, GCS bitstream, replay collapse, TTL handling, relay choice, confirmed graph edges | `GCSFilterTest`, `PacketRelayManagerTest`, `MeshGraphServiceTest`, `TransportBridgeServiceTest`, `AppStateStoreTest` |
| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals, 22h outbound envelope randomization | `NostrProtocolTest` (authenticated-seal cases and outbound timestamp bounds) |
| Application state | Peer unions, canonical private conversations, chronological history, delivery/read behavior, media migration policy | `AppStateStoreTest`, `PrivateChatManagerTest`, `MediaSendingManagerMigrationTest` |
The private-history clock-skew case in `AppStateStoreTest` expects arrival
order, which can differ from timestamp order. That test does not establish the
chronological-history requirement above.
## Golden-vector policy
Golden vectors compare literal externally visible bytes or hashes. Do not update
@ -28,13 +34,35 @@ wire protocol is intentionally versioned and interoperating clients are updated
together.
Round-trip tests remain useful but are not sufficient on their own: an encoder
and decoder can share the same defect. Each critical wire format therefore has
at least one literal vector.
and decoder can share the same defect. Protocol round-trip tests must be paired with literal golden vectors for critical
externally visible formats. Round-trip success alone does not establish compatibility.
NIP-17 receivers should reserve safety slack beyond the maximum timestamp
randomization used by senders. Android caps outbound seal and gift-wrap
randomization at 22h, leaving 2 hours of slack inside iOS's 24-hour
subscription window, while retaining its 48-hour receive lookback.
randomization used by senders. Android's default seal and gift-wrap timestamp
randomization is up to 22h. iOS's DM subscription lookback is 24h, a two-hour
difference. `NostrClient.subscribeToPrivateMessages` requests a 48-hour lookback.
These intervals do not establish delivery under arbitrary delays or clock skew.
Geohash directory downloads and the weekly bundled-asset update use
`relays/online_relays_gps.csv` from the bitchat repo, the source used by iOS
`GeoRelayDirectory`. Android deduplicates parsed rows by a lowercase host key,
omits port 443, includes other ports in 1 through 65535, and
breaks distance ties by that key. `RelayDirectoryTest` covers endpoint normalization, duplicate removal and distance ties.
`RelayDirectoryAddressTest` covers address rejection and port boundaries;
`RelayDirectoryPortTest` covers integer-conversion overflow. Shared synthetic selection
vectors and the Android/Swift check commands are documented in
[the relay-selection test guide](../tools/relay-selection/README.md).
Matching selections depend on the loaded entries. Bundled and cached copies
can differ between clients; a configured source URL does not establish equal
snapshots or successful delivery. Parsing remains permissive: rows with fewer
than three fields or unparseable coordinates are skipped, and duplicate
endpoints keep the first accepted row. Directory addresses accept bare hostnames or
`wss`/`https` URLs with an empty or root path. Userinfo, queries, fragments,
non-root paths and unsupported schemes are skipped; hostname characters are
limited to ASCII letters, digits, dots and hyphens. iOS also checks hosts and coordinate ranges
and rejects a directory containing conflicting duplicate rows. For a host followed by a digit-only port, an integer-conversion failure skips
that row. `RelayDirectoryPortTest` covers oversized ports between valid rows.
Geohash (recipient-less) `bitchat1:` envelopes and their acks carry 8 fresh
random bytes in the embedded packet's sender field, never the mesh peer ID,
@ -56,15 +84,16 @@ From a configured Android development environment, run:
./gradlew clientRewriteContractTest
```
The task runs the new golden vectors and the complete existing unit suite. A
rewrite is compatible only when this gate passes. Tests should be ported
The task runs `:app:testDebugUnitTest`, including the wire contract tests.
It must pass before accepting a rewrite. Passing it does not establish Wear or physical-device compatibility. Tests should be ported
unchanged when package boundaries change; adapter façades are preferable to
weakening assertions.
## Device-only acceptance
Local JVM tests cannot prove Android radio and lifecycle behavior. Before
shipping a rewrite, run the following on at least two physical devices:
Local JVM tests alone do not establish radio operation or lifecycle behavior
on physical Android devices. Before shipping a rewrite, run the following on
at least two physical devices:
1. BLE discovery, connection, disconnect, reconnect, and multi-hop relay.
2. Runtime permission denial/retry for Bluetooth, location, notifications, and

View File

@ -0,0 +1,54 @@
# Relay selection vectors
`app/src/test/resources/contracts/relay-selection-v1/` contains synthetic CSV
files and literal expected URL lists. `cases.tsv` records each case's directory,
query latitude/longitude, requested count and ordered result (`|` separated;
`-` means an empty result).
For accepted directory rows, selection sorts by Haversine distance from the query
coordinate, ascending, then by canonical endpoint key for equal computed distances.
It returns up to `max(count, 0)` URLs. These coordinate-based vectors do not test
geohash decoding or establish identical floating-point ordering for arbitrary
coordinates.
Run the Android production parser and selection helper through the JVM suite:
```sh
./gradlew :app:testDebugUnitTest --tests 'com.bitchat.android.nostr.RelaySelectionVectorTest'
```
With Python 3 and Swift available, check the same cases against a local iOS
source file:
```sh
python3 tools/relay-selection/check_ios.py <ios-checkout>/bitchat/Nostr/GeoRelayDirectory.swift
```
The script reads that file without modifying it or fetching anything. It extracts
its validation policy, entry type, parser, address validator, coordinate-based
selector and distance function into a temporary Swift wrapper. The selected
declarations are unchanged; unrelated application dependencies are excluded.
Source and extracted-declaration hashes identify the implementation checked.
Missing declarations, malformed or empty vector manifests, and compilation failures
stop the check. Source changes
require checking that extraction still covers the relevant dependencies.
Check the local runner’s input and extraction guards with:
```sh
python3 -m unittest discover -s tools/relay-selection -p 'test_*.py'
```
The fixtures cover identical-coordinate ties, reversed row order, lowercase
endpoint keys, default-port duplicates, a nondefault port, requested-count limits,
longitude-dependent order, latitude weighting and selection across the dateline. Both implementations
are compared with literal expectations, rather than generating one client's
expected output from the other. The Android test runs with the unit suite; the
Swift check is an explicit local command and requires the separate source checkout.
The CSVs use reserved example hosts and invented coordinates. They are small,
accepted directories; the Swift parser uses a one-entry minimum without a prior
snapshot. These are selection tests, not remote-download admission tests. They do
not require identical parser acceptance, exercise geohash decoding, or establish
network delivery or physical-device compatibility. Update expectations only after
explaining the intended selection-policy change and checking affected clients.

View File

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Check shared selection vectors using methods extracted from a supplied iOS source file."""
import argparse
import hashlib
import math
from pathlib import Path
import re
import subprocess
import tempfile
def declaration(source, marker):
# Match the declaration's own closing indentation, not nested closure braces.
lines = source.splitlines(keepends=True)
matches = [i for i, line in enumerate(lines) if line.strip().startswith(marker)]
if len(matches) != 1:
raise ValueError(f"Expected exactly one declaration: {marker}")
start = matches[0]
indent = re.match(r" *", lines[start]).group()
for end in range(start + 1, len(lines)):
if lines[end].rstrip() == indent + "}":
return "".join(lines[start:end + 1])
raise ValueError(f"Missing closing brace: {marker}")
def validate_cases(text):
lines = [line for line in text.splitlines() if line.strip()]
if not lines or lines[0] != "id\tcsv\tlatitude\tlongitude\tcount\texpected":
raise ValueError("Invalid vector header")
if len(lines) == 1:
raise ValueError("No selection vectors")
ids = set()
for line in lines[1:]:
f = line.split("\t")
if len(f) != 6:
raise ValueError("Invalid vector fields")
if not f[0].strip() or f[0] in ids:
raise ValueError("Invalid or duplicate vector ID")
ids.add(f[0])
if not re.fullmatch(r"[a-z0-9-]+\.csv", f[1]):
raise ValueError("Invalid vector filename")
lat, lon, count = float(f[2]), float(f[3]), int(f[4])
if not (math.isfinite(lat) and -90 <= lat <= 90 and math.isfinite(lon) and -180 <= lon <= 180):
raise ValueError("Invalid vector coordinates")
if not -(2**31) <= count < 2**31:
raise ValueError("Invalid vector count")
if f[5] != "-" and not all(x.startswith("wss://") and len(x) > 6 for x in f[5].split("|")):
raise ValueError("Invalid vector expectations")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("ios_source", type=Path, help="Read-only GeoRelayDirectory.swift")
parser.add_argument("--fixtures", type=Path, default=Path(__file__).resolve().parents[2] /
"app/src/test/resources/contracts/relay-selection-v1")
args = parser.parse_args()
validate_cases((args.fixtures / "cases.tsv").read_text())
source_bytes = args.ios_source.read_bytes()
source = source_bytes.decode("utf-8")
parts = [declaration(source, marker) for marker in (
"struct GeoRelayDirectoryValidationPolicy:", "struct Entry:",
"nonisolated static func validatedEntries(",
"nonisolated private static func validatedDirectoryAddress(",
"func closestRelays(toLat", "private func haversineKm(")]
# Keep the selected declarations verbatim. Supply only the containing type and entries.
swift = "import Foundation\n" + parts[0] + "\nstruct VectorDirectory {\n" + parts[1]
swift += "var entries: [Entry]\n" + "\n".join(parts[2:5]) + "\n}\n" + parts[5]
swift += r'''
let root = URL(fileURLWithPath: CommandLine.arguments[1])
let rows = try String(contentsOf: root.appendingPathComponent("cases.tsv"), encoding: .utf8)
var failures = 0
var count = 0
for row in rows.split(separator: "\n").dropFirst() {
let f = row.split(separator: "\t", omittingEmptySubsequences: false).map(String.init)
guard f.count == 6, let lat = Double(f[2]), let lon = Double(f[3]), let n = Int(f[4]) else {
fatalError("Malformed vector")
}
let data = try Data(contentsOf: root.appendingPathComponent(f[1]))
guard let entries = VectorDirectory.validatedEntries(from: data, policy: .live, minimumEntries: 1) else {
fatalError("Vector directory rejected: \(f[0])")
}
let expected = f[5] == "-" ? [] : f[5].split(separator: "|").map(String.init)
let actual = VectorDirectory(entries: entries).closestRelays(toLat: lat, lon: lon, count: n)
count += 1
if actual != expected {
failures += 1
print("FAIL \(f[0]): expected \(expected), got \(actual)")
}
}
print("\(count) selection vectors, \(failures) failures")
exit(failures == 0 ? 0 : 1)
'''
print("iOS source SHA256:", hashlib.sha256(source_bytes).hexdigest(), flush=True)
print("Extracted declarations SHA256:", hashlib.sha256("".join(parts).encode()).hexdigest(), flush=True)
with tempfile.TemporaryDirectory(prefix="relay-vectors-") as tmp:
path = Path(tmp)
(path / "main.swift").write_text(swift)
subprocess.run(["swiftc", "-module-cache-path", str(path / "modules"), str(path / "main.swift"), "-o", str(path / "check")], check=True)
result = subprocess.run([str(path / "check"), str(args.fixtures.resolve())])
raise SystemExit(result.returncode)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,38 @@
import unittest
from check_ios import declaration, validate_cases
HEADER = 'id\tcsv\tlatitude\tlongitude\tcount\texpected\n'
ROW = 'one\tties.csv\t0\t0\t1\twss://alpha.example\n'
class VectorInputTest(unittest.TestCase):
def test_valid_manifest(self):
validate_cases(HEADER + ROW)
def test_empty_manifest_rejected(self):
for text in ('', HEADER):
with self.subTest(text=text), self.assertRaises(ValueError):
validate_cases(text)
def test_bad_header_and_duplicate_id_rejected(self):
for text in ('wrong\n' + ROW, HEADER + ROW + ROW):
with self.subTest(text=text), self.assertRaises(ValueError):
validate_cases(text)
def test_malformed_fields_rejected(self):
for row in (ROW.replace('ties.csv', '../ties.csv'), ROW.replace('\t0\t0\t', '\tnan\t0\t'), ROW.replace('\t1\t', '\t2147483648\t'), ROW.replace('wss://alpha.example', '')):
with self.subTest(row=row), self.assertRaises(ValueError):
validate_cases(HEADER + row)
def test_extraction_preserves_nested_declaration(self):
text = 'struct Example {\n func value() {\n if true {\n }\n }\n}\n'
self.assertEqual(' func value() {\n if true {\n }\n }\n', declaration(text, 'func value('))
def test_missing_duplicate_or_unclosed_declaration_rejected(self):
for text in ('', 'struct X {\n}\nstruct X {\n}\n', 'struct X {\n'):
with self.subTest(text=text), self.assertRaises(ValueError):
declaration(text, 'struct X')
if __name__ == '__main__':
unittest.main()