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:
+1
-1
@@ -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),
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+3
-3
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+15
-36
@@ -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),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+3
-3
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
+114
@@ -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),
|
||||||
|
)
|
||||||
+63
@@ -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)
|
||||||
+6
-1
@@ -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")
|
||||||
Reference in New Issue
Block a user