feat(cli,commons): amy login + amy create, share defaults with Amethyst

Move the "defaults a new Amethyst account gets seeded with" into commons
so the UI and amy agree byte-for-byte on what a fresh account looks like.

commons additions:
- commons/defaults/Constants.kt         — relay URL constants (moved from amethyst/)
- commons/defaults/AmethystDefaults.kt  — DefaultChannels, DefaultNIP65RelaySet,
                                          DefaultNIP65List, DefaultGlobalRelays,
                                          DefaultDMRelayList, DefaultSearchRelayList,
                                          DefaultIndexerRelayList (moved from
                                          amethyst/model/AccountSettings.kt)
- commons/account/AccountBootstrapEvents.kt — data class + bootstrapAccountEvents(signer, name)
  that builds the nine events a new Amethyst account publishes: kind:0, 3,
  10002, 10050, 10051, 10099, 50, 51, + channel list. One source of truth.

Retrofits:
- AccountSessionManager.createNewAccount now delegates event construction to
  the commons helper; it still packages them into AccountSettings for the
  Android storage path.
- DefaultSignerPermissions stays in AccountSettings.kt — it uses Android
  NIP-55 Permission/CommandType types.
- 19 import updates across amethyst/ to point at the new commons paths.

New CLI verbs:
- amy create [--name NAME]: mint a keypair, write identity.json, seed
  relays.json with the Amethyst defaults, publish all nine bootstrap events
  to DefaultNIP65RelaySet via NostrClient.publishAndConfirmDetailed.
- amy login KEY [--password X]: accept any identifier form Amethyst's login
  screen accepts — nsec1, ncryptsec (+ --password, NIP-49), BIP-39 mnemonic
  (NIP-06), npub1, nprofile1, 64-hex pubkey (default) or 64-hex privkey
  (with --private), NIP-05 (name@domain.tld, HTTP lookup). Read-only keys
  land with privKeyHex=null; Identity now carries that cleanly.

https://claude.ai/code/session_01M6dCKAF5Y1VyHGZPjzwDXq
This commit is contained in:
Claude
2026-04-21 21:41:47 +00:00
parent 39e11d29c1
commit 559c69660f
27 changed files with 511 additions and 105 deletions
@@ -21,7 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.commons.defaults.Constants
import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
@@ -37,7 +37,6 @@ import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
@@ -55,8 +54,6 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType
import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent
import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
@@ -68,31 +65,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
val DefaultChannels =
listOf(
// Anigma's Nostr
ChannelTag("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", Constants.nos),
// Amethyst's Group
ChannelTag("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", Constants.nos),
)
val DefaultNIP65RelaySet = setOf(Constants.mom, Constants.nos, Constants.bitcoiner)
val DefaultNIP65List =
listOf(
AdvertisedRelayInfo(Constants.mom, AdvertisedRelayType.BOTH),
AdvertisedRelayInfo(Constants.nos, AdvertisedRelayType.BOTH),
AdvertisedRelayInfo(Constants.bitcoiner, AdvertisedRelayType.BOTH),
)
val DefaultGlobalRelays = listOf(Constants.wine, Constants.news)
val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos)
val DefaultSearchRelayList = setOf(Constants.wine, Constants.where, Constants.nostoday, Constants.antiprimal, Constants.ditto)
val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Constants.userkinds, Constants.yabu, Constants.nostr1)
val DefaultSignerPermissions = val DefaultSignerPermissions =
listOf( listOf(
Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND), Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND),
@@ -20,8 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays
import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.NoteState
@@ -20,8 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays
import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList
import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.NoteState
@@ -20,8 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.model.nip65RelayList package com.vitorpamplona.amethyst.model.nip65RelayList
import com.vitorpamplona.amethyst.commons.defaults.Constants
import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.NoteState
@@ -20,8 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.model.topNavFeeds package com.vitorpamplona.amethyst.model.topNavFeeds
import com.vitorpamplona.amethyst.commons.defaults.Constants
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -20,10 +20,10 @@
*/ */
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows
import com.vitorpamplona.amethyst.commons.defaults.Constants
import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
@@ -20,8 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers
import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.BaseEoseManager
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
@@ -58,7 +58,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.commons.defaults.Constants
import com.vitorpamplona.amethyst.service.broadcast.BroadcastEvent import com.vitorpamplona.amethyst.service.broadcast.BroadcastEvent
import com.vitorpamplona.amethyst.service.broadcast.BroadcastStatus import com.vitorpamplona.amethyst.service.broadcast.BroadcastStatus
import com.vitorpamplona.amethyst.service.broadcast.RelayResult import com.vitorpamplona.amethyst.service.broadcast.RelayResult
@@ -77,7 +77,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.commons.defaults.Constants
import com.vitorpamplona.amethyst.service.broadcast.BroadcastEvent import com.vitorpamplona.amethyst.service.broadcast.BroadcastEvent
import com.vitorpamplona.amethyst.service.broadcast.BroadcastStatus import com.vitorpamplona.amethyst.service.broadcast.BroadcastStatus
import com.vitorpamplona.amethyst.service.broadcast.RelayResult import com.vitorpamplona.amethyst.service.broadcast.RelayResult
@@ -23,29 +23,18 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.AccountInfo
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.commons.defaults.DefaultNIP65RelaySet
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.DefaultChannels
import com.vitorpamplona.amethyst.model.DefaultDMRelayList
import com.vitorpamplona.amethyst.model.DefaultGlobalRelays
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.DefaultNIP65List
import com.vitorpamplona.amethyst.model.DefaultNIP65RelaySet
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
@@ -57,12 +46,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49 import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@@ -303,28 +287,23 @@ class AccountSessionManager(
fun createNewAccount(name: String? = null): AccountSettings { fun createNewAccount(name: String? = null): AccountSettings {
val keyPair = KeyPair() val keyPair = KeyPair()
val tempSigner = NostrSignerSync(keyPair) val bootstrap =
com.vitorpamplona.amethyst.commons.account.bootstrapAccountEvents(
signer = NostrSignerSync(keyPair),
name = name,
)
return AccountSettings( return AccountSettings(
keyPair = keyPair, keyPair = keyPair,
transientAccount = false, transientAccount = false,
backupUserMetadata = tempSigner.sign(MetadataEvent.newUser(name)), backupUserMetadata = bootstrap.userMetadata,
backupContactList = backupContactList = bootstrap.contactList,
ContactListEvent.createFromScratch( backupNIP65RelayList = bootstrap.nip65RelayList,
followUsers = listOf(ContactTag(keyPair.pubKey.toHexKey(), null, null)), backupDMRelayList = bootstrap.dmRelayList,
relayUse = emptyMap(), backupKeyPackageRelayList = bootstrap.keyPackageRelayList,
signer = tempSigner, backupSearchRelayList = bootstrap.searchRelayList,
), backupIndexRelayList = bootstrap.indexerRelayList,
backupNIP65RelayList = AdvertisedRelayListEvent.create(DefaultNIP65List, tempSigner), backupChannelList = bootstrap.channelList,
backupDMRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, tempSigner), backupRelayFeedsList = bootstrap.relayFeedsList,
// MIP-00: advertise the default outbox relays as KeyPackage hosts
// so other users can discover and fetch this account's KeyPackage
// events without having to guess where they were published.
backupKeyPackageRelayList = KeyPackageRelayListEvent.create(DefaultNIP65RelaySet.toList(), tempSigner),
backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), tempSigner),
backupIndexRelayList = IndexerRelayListEvent.create(DefaultIndexerRelayList.toList(), tempSigner),
backupChannelList = ChannelListEvent.create(emptyList(), DefaultChannels, tempSigner),
backupRelayFeedsList = RelayFeedsListEvent.create(DefaultGlobalRelays, tempSigner),
) )
} }
@@ -48,10 +48,10 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.defaults.Constants
import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists
import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists import com.vitorpamplona.amethyst.commons.model.toImmutableListOfLists
import com.vitorpamplona.amethyst.model.Constants
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource
import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.commons.defaults.Constants
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource
import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.commons.defaults.Constants
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource
import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.commons.defaults.Constants
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -49,9 +49,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.DefaultDMRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultDMRelayList
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
import com.vitorpamplona.amethyst.ui.components.M3ActionRow import com.vitorpamplona.amethyst.ui.components.M3ActionRow
import com.vitorpamplona.amethyst.ui.components.M3ActionSection import com.vitorpamplona.amethyst.ui.components.M3ActionSection
@@ -38,7 +38,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.DefaultDMRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultDMRelayList
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
@@ -38,7 +38,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
@@ -31,20 +31,36 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.nip19Bech32.toNsec import com.vitorpamplona.quartz.nip19Bech32.toNsec
import java.io.File import java.io.File
/** Persisted identity (hex keys + cached bech32 forms for convenience). */ /**
* Persisted identity.
*
* [privKeyHex] may be null for read-only accounts imported from an `npub`,
* `nprofile` or NIP-05 in that case [nsec] is also null and any CLI verb
* that needs to sign will fail with a clear error. `keyPair()` materialises
* a [KeyPair] with only `pubKey` set when no private key is available.
*/
data class Identity( data class Identity(
val privKeyHex: String, val privKeyHex: String?,
val pubKeyHex: String, val pubKeyHex: String,
val nsec: String, val nsec: String?,
val npub: String, val npub: String,
) { ) {
fun keyPair(): KeyPair = KeyPair(privKey = privKeyHex.hexToByteArray(), pubKey = pubKeyHex.hexToByteArray()) val hasPrivateKey: Boolean get() = privKeyHex != null
fun keyPair(): KeyPair =
if (privKeyHex != null) {
KeyPair(privKey = privKeyHex.hexToByteArray(), pubKey = pubKeyHex.hexToByteArray())
} else {
KeyPair(pubKey = pubKeyHex.hexToByteArray())
}
companion object { companion object {
fun create(): Identity { fun create(): Identity = fromPrivateKey(KeyPair().privKey!!)
val kp = KeyPair()
val priv = kp.privKey!! fun fromNsec(nsec: String): Identity = fromPrivateKey(nsec.bechToBytes())
val pub = kp.pubKey
fun fromPrivateKey(priv: ByteArray): Identity {
val pub = KeyPair(privKey = priv).pubKey
return Identity( return Identity(
privKeyHex = priv.toHexKey(), privKeyHex = priv.toHexKey(),
pubKeyHex = pub.toHexKey(), pubKeyHex = pub.toHexKey(),
@@ -53,17 +69,15 @@ data class Identity(
) )
} }
fun fromNsec(nsec: String): Identity { /** Read-only identity (no private key). */
val priv = nsec.bechToBytes() fun fromPublicKeyHex(pubHex: String): Identity =
val kp = KeyPair(privKey = priv) Identity(
return Identity( privKeyHex = null,
privKeyHex = priv.toHexKey(), pubKeyHex = pubHex.lowercase(),
pubKeyHex = kp.pubKey.toHexKey(), nsec = null,
nsec = priv.toNsec(), npub = pubHex.hexToByteArray().toNpub(),
npub = kp.pubKey.toNpub(),
) )
} }
}
} }
/** /**
@@ -107,6 +107,14 @@ private suspend fun dispatch(argv: Array<String>): Int {
Commands.init(dataDir, Args(tail)) Commands.init(dataDir, Args(tail))
} }
"create" -> {
Commands.create(dataDir, tail)
}
"login" -> {
Commands.login(dataDir, tail)
}
"whoami" -> { "whoami" -> {
Commands.whoami(dataDir) Commands.whoami(dataDir)
} }
@@ -171,7 +179,9 @@ private fun printUsage() {
| amy [--data-dir PATH] <cmd> [args...] | amy [--data-dir PATH] <cmd> [args...]
| |
|Identity: |Identity:
| init [--nsec NSEC] create or import identity | init [--nsec NSEC] create or import a bare identity (no defaults published)
| create [--name NAME] provision a full Amethyst-style account + publish bootstrap events
| login KEY [--password X] import (nsec|ncryptsec|mnemonic|npub|nprofile|hex|nip05)
| whoami print current identity | whoami print current identity
| |
|Relays: |Relays:
@@ -34,6 +34,16 @@ object Commands {
args: Args, args: Args,
): Int = InitCommands.init(dataDir, args) ): Int = InitCommands.init(dataDir, args)
suspend fun create(
dataDir: DataDir,
tail: Array<String>,
): Int = CreateCommand.run(dataDir, tail)
suspend fun login(
dataDir: DataDir,
tail: Array<String>,
): Int = LoginCommand.run(dataDir, tail)
suspend fun whoami(dataDir: DataDir): Int = InitCommands.whoami(dataDir) suspend fun whoami(dataDir: DataDir): Int = InitCommands.whoami(dataDir)
suspend fun relay( suspend fun relay(
@@ -0,0 +1,114 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.cli.commands
import com.vitorpamplona.amethyst.cli.Args
import com.vitorpamplona.amethyst.cli.Context
import com.vitorpamplona.amethyst.cli.DataDir
import com.vitorpamplona.amethyst.cli.Identity
import com.vitorpamplona.amethyst.cli.Json
import com.vitorpamplona.amethyst.cli.RelayConfig
import com.vitorpamplona.amethyst.commons.account.bootstrapAccountEvents
import com.vitorpamplona.amethyst.commons.defaults.DefaultDMRelayList
import com.vitorpamplona.amethyst.commons.defaults.DefaultNIP65List
import com.vitorpamplona.amethyst.commons.defaults.DefaultNIP65RelaySet
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
/**
* `amy create [--name NAME]` provision a brand-new Nostr account with the
* same defaults Amethyst uses, publish the nine bootstrap events to the
* default NIP-65 relay set, and seed this data-dir's relay config so
* subsequent `amy marmot ` commands immediately target the right relays.
*
* The heavy lifting (which events to sign, with which defaults) lives in
* `commons/.../AccountBootstrap.kt` so this command stays assembly-thin
* and the on-relay shape matches the in-app flow byte-for-byte.
*/
object CreateCommand {
suspend fun run(
dataDir: DataDir,
rest: Array<String>,
): Int {
if (dataDir.loadIdentityOrNull() != null) {
return Json.error("exists", "identity already exists at ${dataDir.identityFile}")
}
val args = Args(rest)
val name = args.flag("name")
// 1. Mint identity + seed relay config.
val identity = Identity.create()
dataDir.saveIdentity(identity)
dataDir.saveRelays(defaultRelayConfig())
// 2. Build the nine signed bootstrap events via the shared helper.
val signer = NostrSignerSync(identity.keyPair())
val bootstrap = bootstrapAccountEvents(signer, name)
// 3. Open a Context so we reuse publishAndConfirmDetailed + the
// NostrClient plumbing instead of reinventing a second client.
val ctx = Context.open(dataDir)
val accepted = mutableMapOf<String, List<String>>()
try {
ctx.prepare()
for (event in bootstrap.all()) {
val ack = ctx.publish(event, DefaultNIP65RelaySet)
accepted[event.kind.toString()] =
ack.filterValues { it }.keys.map { it.url }
}
} finally {
ctx.close()
}
Json.writeLine(
mapOf(
"npub" to identity.npub,
"hex" to identity.pubKeyHex,
"name" to (name ?: ""),
"data_dir" to dataDir.root.absolutePath,
"published_kinds" to bootstrap.all().map { it.kind },
"accepted_by" to accepted,
"relays" to
mapOf(
"nip65" to DefaultNIP65List.map { it.relayUrl.url },
"inbox" to DefaultDMRelayList.map { it.url },
"key_package" to DefaultNIP65RelaySet.map { it.url },
),
),
)
return 0
}
/**
* Mirror of what Amethyst's in-app defaults would write on disk NIP-65
* outbox, DM inbox (kind:10050), and KeyPackage host relays (kind:10051).
* Keeping these in sync with the signed events above is load-bearing:
* `amy marmot key-package publish` later reads `relays.json` to decide
* where to publish, and if those diverge from the advertised kind:10051
* nobody will find the KPs.
*/
private fun defaultRelayConfig(): RelayConfig {
val cfg = RelayConfig()
DefaultNIP65List.forEach { cfg.add("nip65", it.relayUrl.url) }
DefaultDMRelayList.forEach { cfg.add("inbox", it.url) }
DefaultNIP65RelaySet.forEach { cfg.add("key_package", it.url) }
return cfg
}
}
@@ -0,0 +1,125 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.cli.commands
import com.vitorpamplona.amethyst.cli.Args
import com.vitorpamplona.amethyst.cli.DataDir
import com.vitorpamplona.amethyst.cli.Identity
import com.vitorpamplona.amethyst.cli.Json
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Client
import com.vitorpamplona.quartz.nip05DnsIdentifiers.OkHttpNip05Fetcher
import com.vitorpamplona.quartz.nip05DnsIdentifiers.resolveUserHexOrNull
import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06
import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49
import okhttp3.OkHttpClient
/**
* `amy login KEY [--password X]` import any of the identifier forms
* Amethyst's login screen accepts and persist the identity to the
* data-dir. Mirrors [AccountSessionManager.loginSync] but on the JVM.
*
* Accepted forms (tried in this order):
* - nsec1 full account
* - ncryptsec + --password X NIP-49 decrypt full
* - BIP-39 mnemonic (space-separated) NIP-06 derive full
* - 64-hex private key (with --private) full
* - npub1 / nprofile1 / 64-hex pubkey read-only
* - NIP-05 identifier (name@domain.tld) read-only (HTTP lookup)
*/
object LoginCommand {
suspend fun run(
dataDir: DataDir,
rest: Array<String>,
): Int {
if (rest.isEmpty()) {
return Json.error("bad_args", "login <nsec|ncryptsec|mnemonic|npub|nprofile|hex|nip05> [--password X]")
}
if (dataDir.loadIdentityOrNull() != null) {
return Json.error("exists", "identity already exists at ${dataDir.identityFile}; use a fresh --data-dir or delete it first")
}
val key = rest[0].trim()
val args = Args(rest.drop(1).toTypedArray())
val identity =
resolveIdentity(key, args)
?: return Json.error(
"bad_key",
"could not parse '$key' as any supported identifier",
)
dataDir.saveIdentity(identity)
Json.writeLine(
mapOf(
"npub" to identity.npub,
"hex" to identity.pubKeyHex,
"read_only" to !identity.hasPrivateKey,
"data_dir" to dataDir.root.absolutePath,
),
)
return 0
}
private suspend fun resolveIdentity(
key: String,
args: Args,
): Identity? {
// 1. ncryptsec — password mandatory.
if (key.startsWith("ncryptsec")) {
val pw =
args.flag("password") ?: args.flag("pw")
?: throw IllegalArgumentException("ncryptsec input requires --password")
val privHex = Nip49().decrypt(key, pw)
return Identity.fromPrivateKey(
com.vitorpamplona.quartz.utils.Hex
.decode(privHex),
)
}
// 2. nsec
if (key.startsWith("nsec1")) return Identity.fromNsec(key)
// 3. mnemonic (space-separated, 12/24 words)
if (key.contains(' ') && Nip06().isValidMnemonic(key)) {
val priv = Nip06().privateKeyFromMnemonic(key)
return Identity.fromPrivateKey(priv)
}
// 4. 64-hex privkey — only when explicitly asked; otherwise a bare
// hex string is ambiguous with a pubkey and we default to public.
if (args.bool("private") && isHex64(key)) {
return Identity.fromPrivateKey(
com.vitorpamplona.quartz.utils.Hex
.decode(key),
)
}
// 5. everything else — defer to the shared resolver (npub / nprofile /
// hex pubkey / NIP-05). Read-only.
val pubHex = resolveUserHexOrNull(key, nip05Client()) ?: return null
return Identity.fromPublicKeyHex(pubHex)
}
private fun isHex64(s: String): Boolean = s.length == 64 && s.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' }
private fun nip05Client(): Nip05Client {
// Build a throwaway OkHttp client — we don't hold a Context here and
// login is a one-shot CLI invocation anyway.
val http = OkHttpClient.Builder().build()
return Nip05Client(fetcher = OkHttpNip05Fetcher { _ -> http })
}
}
@@ -0,0 +1,114 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.account
import com.vitorpamplona.amethyst.commons.defaults.DefaultChannels
import com.vitorpamplona.amethyst.commons.defaults.DefaultDMRelayList
import com.vitorpamplona.amethyst.commons.defaults.DefaultGlobalRelays
import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList
import com.vitorpamplona.amethyst.commons.defaults.DefaultNIP65List
import com.vitorpamplona.amethyst.commons.defaults.DefaultNIP65RelaySet
import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent
import com.vitorpamplona.quartz.nip51Lists.relayLists.RelayFeedsListEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
/**
* Full set of "new Amethyst account" bootstrap events: the nine signed
* events that [com.vitorpamplona.amethyst.ui.screen.AccountSessionManager.createNewAccount]
* produces for a brand-new user, in the exact same order and with the same
* relay defaults. Used by:
*
* - Amethyst's in-app "Create Account" flow (builds an `AccountSettings`
* from these).
* - `amy create` (writes the keypair to disk, publishes these to the
* default NIP-65 relay set).
*
* Each event is optional in the sense that the builder lets you null out
* individual ones via the args but the sensible default is "all of them".
*/
data class AccountBootstrapEvents(
val userMetadata: MetadataEvent,
val contactList: ContactListEvent,
val nip65RelayList: AdvertisedRelayListEvent,
val dmRelayList: ChatMessageRelayListEvent,
val keyPackageRelayList: KeyPackageRelayListEvent,
val searchRelayList: SearchRelayListEvent,
val indexerRelayList: IndexerRelayListEvent,
val channelList: ChannelListEvent,
val relayFeedsList: RelayFeedsListEvent,
) {
/** All nine signed events in publication order. */
fun all(): List<com.vitorpamplona.quartz.nip01Core.core.Event> =
listOf(
userMetadata,
contactList,
nip65RelayList,
dmRelayList,
keyPackageRelayList,
searchRelayList,
indexerRelayList,
channelList,
relayFeedsList,
)
}
/**
* Build and sign the nine events that comprise a freshly-created Amethyst
* account. The caller is responsible for publishing them typically to
* [DefaultNIP65RelaySet] after at least a short delay so relay
* connections are established.
*
* [name] is the optional display name; null or blank produces a kind:0
* event with no `name` field.
*/
fun bootstrapAccountEvents(
signer: NostrSignerSync,
name: String? = null,
): AccountBootstrapEvents =
AccountBootstrapEvents(
userMetadata = signer.sign(MetadataEvent.newUser(name)),
contactList =
ContactListEvent.createFromScratch(
followUsers = listOf(ContactTag(signer.keyPair.pubKey.toHexKey(), null, null)),
relayUse = emptyMap(),
signer = signer,
),
nip65RelayList = AdvertisedRelayListEvent.create(DefaultNIP65List, signer),
dmRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, signer),
// MIP-00: advertise the default outbox relays as KeyPackage hosts
// so other users can discover and fetch this account's KeyPackage
// events without having to guess where they were published.
keyPackageRelayList = KeyPackageRelayListEvent.create(DefaultNIP65RelaySet.toList(), signer),
searchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), signer),
indexerRelayList = IndexerRelayListEvent.create(DefaultIndexerRelayList.toList(), signer),
channelList = ChannelListEvent.create(emptyList(), DefaultChannels, signer),
relayFeedsList = RelayFeedsListEvent.create(DefaultGlobalRelays, signer),
)
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.defaults
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType
/**
* Default relay/channel bundles applied to every freshly-created Amethyst
* account. Both the Android UI (`AccountSessionManager.createNewAccount`)
* and the `amy` CLI (`amy create`) seed new accounts from these so users
* land in the same connected state regardless of which client they start
* with.
*
* Pure data no platform deps, no runtime config, no i18n. Change here,
* both clients follow.
*/
val DefaultChannels =
listOf(
// Anigma's Nostr
ChannelTag("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", Constants.nos),
// Amethyst's Group
ChannelTag("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", Constants.nos),
)
val DefaultNIP65RelaySet = setOf(Constants.mom, Constants.nos, Constants.bitcoiner)
val DefaultNIP65List =
listOf(
AdvertisedRelayInfo(Constants.mom, AdvertisedRelayType.BOTH),
AdvertisedRelayInfo(Constants.nos, AdvertisedRelayType.BOTH),
AdvertisedRelayInfo(Constants.bitcoiner, AdvertisedRelayType.BOTH),
)
val DefaultGlobalRelays = listOf(Constants.wine, Constants.news)
val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos)
val DefaultSearchRelayList =
setOf(Constants.wine, Constants.where, Constants.nostoday, Constants.antiprimal, Constants.ditto)
val DefaultIndexerRelayList =
setOf(Constants.purplepages, Constants.coracle, Constants.userkinds, Constants.yabu, Constants.nostr1)
@@ -18,10 +18,15 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.commons.defaults
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
/**
* Relay URLs that Amethyst (and `amy`) seed new accounts with and use as
* sensible defaults across the app. Pure data lives in commons so JVM /
* iOS / desktop builds get the same URLs without duplicating the list.
*/
object Constants { object Constants {
val nos = RelayUrlNormalizer.normalize("wss://nos.lol") val nos = RelayUrlNormalizer.normalize("wss://nos.lol")
val mom = RelayUrlNormalizer.normalize("wss://nostr.mom") val mom = RelayUrlNormalizer.normalize("wss://nostr.mom")