mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 17:23:21 +00:00
Merge pull request #1825 from vitorpamplona/claude/event-sync-screen-sYGtN
Add Event Sync feature to redistribute events across relays
This commit is contained in:
@@ -1,69 +0,0 @@
|
||||
name: Build APK For Claude
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'claude/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-benchmark:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: 21
|
||||
|
||||
- name: Cache gradle
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Build Benchmark APK
|
||||
run: ./gradlew assemblePlayBenchmark
|
||||
|
||||
- name: Upload Play Benchmark APK
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Play Benchmark APK
|
||||
path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk
|
||||
|
||||
- name: Comment on PR with APK link
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const artifactId = `${{ steps.upload.outputs.artifact-id }}`;
|
||||
const downloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${artifactId}`;
|
||||
const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${downloadUrl})`;
|
||||
|
||||
const branch = context.ref.replace('refs/heads/', '');
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: `${context.repo.owner}:${branch}`,
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
for (const pr of prs) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body
|
||||
});
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.relays.eventsync
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.model.Constants
|
||||
import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class EventSyncTest {
|
||||
companion object {
|
||||
val vitor = "wss://vitor.nostr1.com".normalizeRelayUrl()
|
||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
val rootClient =
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.addInterceptor(DefaultContentTypeInterceptor("Amethyst/v1.05"))
|
||||
.build()
|
||||
val socketBuilder = BasicOkHttpWebSocket.Builder { url -> rootClient }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSync() =
|
||||
runBlocking {
|
||||
val sync =
|
||||
EventSync(
|
||||
accountPubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||
relayDb = {
|
||||
listOf(Constants.mom, Constants.nos)
|
||||
},
|
||||
outboxTargets = { setOf(vitor) },
|
||||
inboxTargets = { setOf(vitor) },
|
||||
dmTargets = { setOf(vitor) },
|
||||
clientBuilder = {
|
||||
NostrClient(socketBuilder, appScope)
|
||||
},
|
||||
scope = appScope,
|
||||
)
|
||||
|
||||
sync.runSync()
|
||||
}
|
||||
}
|
||||
@@ -171,8 +171,8 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
|
||||
@@ -135,8 +135,8 @@ import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent
|
||||
import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore
|
||||
import com.vitorpamplona.quartz.nip40Expiration.isExpired
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
|
||||
+6
-6
@@ -31,13 +31,13 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NostrWalletConnectRequestCache
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NostrWalletConnectResponseCache
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectRequestCache
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.cache.NostrWalletConnectResponseCache
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@@ -30,7 +30,7 @@ import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.rsvp.CalendarRSVPEvent
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
|
||||
fun filterNWCPaymentsFromRequests(
|
||||
serviceKeys: Set<HexKey>,
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.KeyDataSourceSubscription
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Composable
|
||||
|
||||
@@ -110,6 +110,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.redirect.LoadRedirectScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relay.RelayFeedScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSyncScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen
|
||||
@@ -217,6 +218,7 @@ fun AppNavigation(
|
||||
composableFromEndArgs<Route.Nip47NWCSetup> { NIP47SetupScreen(accountViewModel, nav, it.nip47) }
|
||||
composableFromEndArgs<Route.UpdateZapAmount> { UpdateZapAmountScreen(accountViewModel, nav, it.nip47) }
|
||||
composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.EventSync> { EventSyncScreen(accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.EditMediaServers> { AllMediaServersScreen(accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.UpdateReactionType> { UpdateReactionTypeScreen(accountViewModel, nav) }
|
||||
|
||||
|
||||
+9
@@ -51,6 +51,7 @@ import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
import androidx.compose.material.icons.outlined.Drafts
|
||||
import androidx.compose.material.icons.outlined.GroupAdd
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.Sync
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -481,6 +482,14 @@ fun ListContent(
|
||||
route = Route.Chess,
|
||||
)
|
||||
|
||||
NavigationRow(
|
||||
title = R.string.event_sync_title,
|
||||
icon = Icons.Outlined.Sync,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
nav = nav,
|
||||
route = Route.EventSync,
|
||||
)
|
||||
|
||||
NavigationRow(
|
||||
title = R.string.route_import_follows,
|
||||
icon = Icons.Outlined.GroupAdd,
|
||||
|
||||
@@ -134,6 +134,8 @@ sealed class Route {
|
||||
|
||||
@Serializable object EditRelays : Route()
|
||||
|
||||
@Serializable object EventSync : Route()
|
||||
|
||||
@Serializable object EditMediaServers : Route()
|
||||
|
||||
@Serializable object UpdateReactionType : Route()
|
||||
|
||||
@@ -32,10 +32,16 @@ import kotlin.math.round
|
||||
private const val YEAR_DATE_FORMAT = "MMM dd, yyyy"
|
||||
private const val MONTH_DATE_FORMAT = "MMM dd"
|
||||
|
||||
private const val YEAR_NO_DAY_DATE_FORMAT = "MMM yyyy"
|
||||
private const val MONTH_NO_DAY_DATE_FORMAT = "MMM dd"
|
||||
|
||||
var locale: Locale = Locale.getDefault()
|
||||
var yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale)
|
||||
var monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale)
|
||||
|
||||
var yearNoDayFormatter = SimpleDateFormat(YEAR_NO_DAY_DATE_FORMAT, locale)
|
||||
var monthNoDayFormatter = SimpleDateFormat(MONTH_NO_DAY_DATE_FORMAT, locale)
|
||||
|
||||
fun timeAgo(
|
||||
time: Long?,
|
||||
context: Context,
|
||||
@@ -116,6 +122,46 @@ fun timeAgoNoDot(
|
||||
}
|
||||
}
|
||||
|
||||
fun timeAgoNoDotNoDay(
|
||||
time: Long?,
|
||||
context: Context,
|
||||
): String {
|
||||
if (time == null) return " "
|
||||
if (time == 0L) return " ${stringRes(context, R.string.never)}"
|
||||
|
||||
val timeDifference = TimeUtils.now() - time
|
||||
|
||||
return if (timeDifference > TimeUtils.ONE_YEAR) {
|
||||
// Dec 12, 2022
|
||||
|
||||
if (locale != Locale.getDefault()) {
|
||||
locale = Locale.getDefault()
|
||||
yearNoDayFormatter = SimpleDateFormat(YEAR_NO_DAY_DATE_FORMAT, locale)
|
||||
monthNoDayFormatter = SimpleDateFormat(MONTH_NO_DAY_DATE_FORMAT, locale)
|
||||
}
|
||||
|
||||
yearNoDayFormatter.format(time * 1000)
|
||||
} else if (timeDifference > TimeUtils.ONE_MONTH) {
|
||||
// Dec 12
|
||||
if (locale != Locale.getDefault()) {
|
||||
locale = Locale.getDefault()
|
||||
yearNoDayFormatter = SimpleDateFormat(YEAR_NO_DAY_DATE_FORMAT, locale)
|
||||
monthNoDayFormatter = SimpleDateFormat(MONTH_NO_DAY_DATE_FORMAT, locale)
|
||||
}
|
||||
|
||||
monthNoDayFormatter.format(time * 1000)
|
||||
} else if (timeDifference > TimeUtils.ONE_DAY) {
|
||||
// 2 days
|
||||
(timeDifference / TimeUtils.ONE_DAY).toString() + stringRes(context, R.string.d)
|
||||
} else if (timeDifference > TimeUtils.ONE_HOUR) {
|
||||
(timeDifference / TimeUtils.ONE_HOUR).toString() + stringRes(context, R.string.h)
|
||||
} else if (timeDifference > TimeUtils.ONE_MINUTE) {
|
||||
(timeDifference / TimeUtils.ONE_MINUTE).toString() + stringRes(context, R.string.m)
|
||||
} else {
|
||||
stringRes(context, R.string.now)
|
||||
}
|
||||
}
|
||||
|
||||
fun timeAheadNoDot(
|
||||
time: Long?,
|
||||
context: Context,
|
||||
|
||||
+67
-1
@@ -35,6 +35,7 @@ import coil3.asDrawable
|
||||
import coil3.imageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
|
||||
@@ -79,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
|
||||
import com.vitorpamplona.amethyst.ui.screen.UiSettingsState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||
@@ -93,8 +95,10 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.EmptyIAuthStatus
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthenticator
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
@@ -123,7 +127,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
@@ -144,8 +148,10 @@ import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -175,6 +181,66 @@ class AccountViewModel(
|
||||
val broadcastTracker = BroadcastTracker()
|
||||
val feedStates = AccountFeedContentStates(account, viewModelScope)
|
||||
|
||||
val eventSync =
|
||||
EventSync(
|
||||
accountPubKey = account.signer.pubKey,
|
||||
relayDb = {
|
||||
val stats = Amethyst.instance.relayStats.snapshot()
|
||||
|
||||
val relays =
|
||||
account.cache.relayHints.relayDB
|
||||
.keys()
|
||||
.filter { url ->
|
||||
val relayStat = stats[url]
|
||||
// has connected at least once OR never tried.
|
||||
if (relayStat != null) {
|
||||
relayStat.connectionCompleted > 0 || relayStat.connectionTentatives == 0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
val sortMap = relays.associateWith { stats.get(it)?.receivedBytes }
|
||||
|
||||
relays.sortedByDescending { sortMap[it] }
|
||||
},
|
||||
outboxTargets = { account.nip65RelayList.outboxFlow.value },
|
||||
inboxTargets = { account.nip65RelayList.inboxFlow.value },
|
||||
dmTargets = { account.dmRelayList.flow.value },
|
||||
clientBuilder = {
|
||||
// creates a new client to make sure these events don't end up polluting the local cache.
|
||||
|
||||
// Create a new scope that inherits the ViewModel's lifecycle
|
||||
// but uses a SupervisorJob so child failures are independent.
|
||||
val customScope = CoroutineScope(viewModelScope.coroutineContext + SupervisorJob())
|
||||
|
||||
// Provides a relay pool
|
||||
val newClient = NostrClient(Amethyst.instance.websocketBuilder, customScope)
|
||||
|
||||
// Authenticates with relays.
|
||||
val auth =
|
||||
RelayAuthenticator(
|
||||
newClient,
|
||||
customScope,
|
||||
signWithAllLoggedInUsers = { authTemplate ->
|
||||
if (account.signer.isWriteable()) {
|
||||
try {
|
||||
listOf(account.signer.sign(authTemplate))
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthCoordinator", "Failed trying to authenticate a writeable account", e)
|
||||
emptyList()
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
newClient
|
||||
},
|
||||
scope = viewModelScope,
|
||||
)
|
||||
|
||||
val tempManualPaymentCache = LruCache<String, List<ZapPaymentHandler.Payable>>(5)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ import com.vitorpamplona.amethyst.ui.theme.SimpleImage75Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppMetadata
|
||||
import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent
|
||||
|
||||
+2
-2
@@ -46,8 +46,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
|
||||
@Composable
|
||||
fun DisplayLNAddress(
|
||||
|
||||
+2
-2
@@ -191,8 +191,8 @@ import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayList
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
|
||||
+636
@@ -0,0 +1,636 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.relays.eventsync
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync.Companion.MAX_ACTIVITY_LOG
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync.Companion.MAX_CONCURRENT_RELAYS
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSync.LiveSyncActivity.SourceRelayInfo
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.reqBypassingRelayLimits
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/**
|
||||
* Syncs the user's events across all known relays:
|
||||
* 1. Downloads all events authored by the user and sends them to their outbox relays.
|
||||
* 2. Downloads all events that p-tag the user (non-DM) and sends them to their inbox relays.
|
||||
* 3. Downloads kind-4 and kind-1059 events that p-tag the user and sends them to DM relays.
|
||||
*
|
||||
* Up to [MAX_CONCURRENT_RELAYS] relays are queried in parallel. As soon as one relay is fully
|
||||
* exhausted (all pages retrieved) the next relay from the list starts immediately, keeping the
|
||||
* concurrency window full at all times.
|
||||
*
|
||||
* Each relay is paginated individually: after EOSE the oldest [Event.createdAt] seen on that
|
||||
* relay becomes the next `until` cursor, repeating until the relay returns no new events.
|
||||
*
|
||||
* OK (true) responses from destination relays are tracked via [IRelayClientListener] and
|
||||
* attributed back to the source relay that contributed each event.
|
||||
*
|
||||
* Live activity is emitted via [liveActivity] so the UI can show a per-relay log of events
|
||||
* received and events accepted by destination relays.
|
||||
*
|
||||
* Scoped to the AccountViewModel so the sync survives navigation within the same session.
|
||||
*/
|
||||
@Stable
|
||||
class EventSync(
|
||||
private val accountPubKey: HexKey,
|
||||
private val relayDb: () -> List<NormalizedRelayUrl>,
|
||||
private val outboxTargets: () -> Set<NormalizedRelayUrl>,
|
||||
private val inboxTargets: () -> Set<NormalizedRelayUrl>,
|
||||
private val dmTargets: () -> Set<NormalizedRelayUrl>,
|
||||
private val clientBuilder: () -> INostrClient,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
companion object {
|
||||
/** Maximum number of relays queried at the same time. */
|
||||
const val MAX_CONCURRENT_RELAYS = 50
|
||||
|
||||
/** How long (ms) to wait for a single relay to reply per page before giving up. */
|
||||
const val RELAY_TIMEOUT_MS = 30_000L
|
||||
|
||||
/** Maximum number of completed-relay entries kept in the activity log. */
|
||||
const val MAX_ACTIVITY_LOG = 5000
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public state
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
sealed class SyncState {
|
||||
object Idle : SyncState()
|
||||
|
||||
data class Running(
|
||||
val relaysCompleted: MutableStateFlow<Int>,
|
||||
val totalRelays: MutableStateFlow<Int>,
|
||||
val eventsSent: MutableStateFlow<Int>,
|
||||
val eventsReceived: MutableStateFlow<Int>,
|
||||
val eventsAccepted: MutableStateFlow<Int>,
|
||||
) : SyncState() {
|
||||
constructor(relaysCompleted: Int, totalRelays: Int, eventsSent: Int, eventsReceived: Int, eventsAccepted: Int) :
|
||||
this(
|
||||
relaysCompleted = MutableStateFlow(relaysCompleted),
|
||||
totalRelays = MutableStateFlow(totalRelays),
|
||||
eventsSent = MutableStateFlow(eventsSent),
|
||||
eventsReceived = MutableStateFlow(eventsReceived),
|
||||
eventsAccepted = MutableStateFlow(eventsAccepted),
|
||||
)
|
||||
}
|
||||
|
||||
data class Done(
|
||||
val totalEventsReceived: Int,
|
||||
val totalEventsSent: Int,
|
||||
val totalEventsAccepted: Int,
|
||||
val durationMs: Long,
|
||||
) : SyncState()
|
||||
|
||||
data class Error(
|
||||
val message: String,
|
||||
) : SyncState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-relay activity snapshot emitted continuously while the sync runs.
|
||||
*
|
||||
* @param completedRelays Last [MAX_ACTIVITY_LOG] relays that finished, sorted by most events
|
||||
* found (descending) so the most productive sources appear first.
|
||||
* @param outboxTargets Relays receiving events authored by the user.
|
||||
* @param inboxTargets Relays receiving events that mention the user.
|
||||
* @param dmTargets Relays receiving DMs addressed to the user.
|
||||
*/
|
||||
@Stable
|
||||
data class LiveSyncActivity(
|
||||
val runningRelays: Map<NormalizedRelayUrl, SourceRelayInfo> = emptyMap(),
|
||||
val completedRelays: Map<NormalizedRelayUrl, SourceRelayInfo> = emptyMap(),
|
||||
val outboxTargets: Map<NormalizedRelayUrl, DestinationRelayInfo> = emptyMap(),
|
||||
val inboxTargets: Map<NormalizedRelayUrl, DestinationRelayInfo> = emptyMap(),
|
||||
val dmTargets: Map<NormalizedRelayUrl, DestinationRelayInfo> = emptyMap(),
|
||||
) {
|
||||
companion object {
|
||||
val DefaultOrder = compareByDescending<SourceRelayInfo> { it.eventsFound.value }.thenByDescending { it.status.value == ConnectionStatus.Completed }
|
||||
}
|
||||
|
||||
val sortedCompletedRelays = completedRelays.values.sortedWith(DefaultOrder)
|
||||
|
||||
constructor(
|
||||
runningRelays: List<SourceRelayInfo>,
|
||||
completedRelays: List<SourceRelayInfo>,
|
||||
outboxTargets: List<DestinationRelayInfo>,
|
||||
inboxTargets: List<DestinationRelayInfo>,
|
||||
dmTargets: List<DestinationRelayInfo>,
|
||||
) : this(
|
||||
runningRelays.associateBy { it.relay },
|
||||
completedRelays.associateBy { it.relay },
|
||||
outboxTargets.associateBy { it.relay },
|
||||
inboxTargets.associateBy { it.relay },
|
||||
dmTargets.associateBy { it.relay },
|
||||
)
|
||||
|
||||
sealed interface ConnectionStatus {
|
||||
object Connecting : ConnectionStatus
|
||||
|
||||
object Querying : ConnectionStatus
|
||||
|
||||
class Error(
|
||||
val msg: String,
|
||||
) : ConnectionStatus
|
||||
|
||||
object Completed : ConnectionStatus
|
||||
}
|
||||
|
||||
/**
|
||||
* @param eventsFound Total events received from this relay across all pages.
|
||||
* @param eventsAccepted Events from this relay that destination relays accepted as new
|
||||
* (OK true). Reflects the count at relay-completion time; late
|
||||
* OK responses may not be included.
|
||||
*/
|
||||
@Stable
|
||||
data class SourceRelayInfo(
|
||||
val relay: NormalizedRelayUrl,
|
||||
val status: MutableStateFlow<ConnectionStatus>,
|
||||
val eventsFound: MutableStateFlow<Int>,
|
||||
val eventsAccepted: MutableStateFlow<Int>,
|
||||
val pageUntil: MutableStateFlow<Long?> = MutableStateFlow(null),
|
||||
) {
|
||||
constructor(relay: NormalizedRelayUrl, status: ConnectionStatus, eventsFound: Int, eventsAccepted: Int, untilPage: Long? = null) :
|
||||
this(
|
||||
relay = relay,
|
||||
status = MutableStateFlow(status),
|
||||
eventsFound = MutableStateFlow(eventsFound),
|
||||
eventsAccepted = MutableStateFlow(eventsAccepted),
|
||||
pageUntil = MutableStateFlow(untilPage),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param relay The destination relay URL.
|
||||
* @param eventsSent Number of events sent to this relay.
|
||||
* @param eventsAccepted Number of OK=true responses received from this relay.
|
||||
*/
|
||||
@Stable
|
||||
data class DestinationRelayInfo(
|
||||
val relay: NormalizedRelayUrl,
|
||||
val eventsSent: MutableStateFlow<Int>,
|
||||
val eventsAccepted: MutableStateFlow<Int>,
|
||||
) {
|
||||
constructor(relay: NormalizedRelayUrl, eventsSent: Int, eventsAccepted: Int) :
|
||||
this(relay, MutableStateFlow(eventsSent), MutableStateFlow(eventsAccepted))
|
||||
}
|
||||
}
|
||||
|
||||
private val _syncState = MutableStateFlow<SyncState>(SyncState.Idle)
|
||||
val syncState: StateFlow<SyncState> = _syncState
|
||||
|
||||
private val _liveActivity = MutableStateFlow(LiveSyncActivity())
|
||||
val liveActivity: StateFlow<LiveSyncActivity> = _liveActivity
|
||||
|
||||
private fun emitLiveSnapshot(
|
||||
runningRelays: Set<NormalizedRelayUrl> = emptySet(),
|
||||
completedRelays: Set<NormalizedRelayUrl> = emptySet(),
|
||||
liveOutboxTargets: Set<NormalizedRelayUrl> = emptySet(),
|
||||
liveInboxTargets: Set<NormalizedRelayUrl> = emptySet(),
|
||||
liveDmTargets: Set<NormalizedRelayUrl> = emptySet(),
|
||||
) {
|
||||
_liveActivity.value =
|
||||
LiveSyncActivity(
|
||||
runningRelays =
|
||||
runningRelays.associateWith {
|
||||
SourceRelayInfo(
|
||||
relay = it,
|
||||
status = MutableStateFlow(LiveSyncActivity.ConnectionStatus.Connecting),
|
||||
eventsFound = MutableStateFlow<Int>(0),
|
||||
eventsAccepted = MutableStateFlow<Int>(0),
|
||||
)
|
||||
},
|
||||
completedRelays =
|
||||
completedRelays.associateWith {
|
||||
SourceRelayInfo(
|
||||
relay = it,
|
||||
status = MutableStateFlow(LiveSyncActivity.ConnectionStatus.Connecting),
|
||||
eventsFound = MutableStateFlow<Int>(0),
|
||||
eventsAccepted = MutableStateFlow<Int>(0),
|
||||
)
|
||||
},
|
||||
outboxTargets =
|
||||
liveOutboxTargets.associateWith { relay ->
|
||||
LiveSyncActivity.DestinationRelayInfo(
|
||||
relay = relay,
|
||||
eventsSent = MutableStateFlow<Int>(0),
|
||||
eventsAccepted = MutableStateFlow<Int>(0),
|
||||
)
|
||||
},
|
||||
inboxTargets =
|
||||
liveInboxTargets.associateWith { relay ->
|
||||
LiveSyncActivity.DestinationRelayInfo(
|
||||
relay = relay,
|
||||
eventsSent = MutableStateFlow<Int>(0),
|
||||
eventsAccepted = MutableStateFlow<Int>(0),
|
||||
)
|
||||
},
|
||||
dmTargets =
|
||||
liveDmTargets.associateWith { relay ->
|
||||
LiveSyncActivity.DestinationRelayInfo(
|
||||
relay = relay,
|
||||
eventsSent = MutableStateFlow<Int>(0),
|
||||
eventsAccepted = MutableStateFlow<Int>(0),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Control functions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private var syncJob: Job? = null
|
||||
|
||||
fun start() {
|
||||
if (_syncState.value is SyncState.Running) return
|
||||
_liveActivity.value = LiveSyncActivity()
|
||||
syncJob =
|
||||
scope.launch(Dispatchers.IO) {
|
||||
runSync()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
syncJob?.cancel()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Sync logic
|
||||
// -------------------------------------------------------------------------
|
||||
suspend fun runSync() {
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
_liveActivity.value = LiveSyncActivity()
|
||||
|
||||
val myPubKey = accountPubKey
|
||||
|
||||
val relaysToProcess = relayDb()
|
||||
|
||||
if (relaysToProcess.isEmpty()) {
|
||||
_syncState.value =
|
||||
SyncState.Error("No known relays found. Browse some content first to discover relays.")
|
||||
return
|
||||
}
|
||||
|
||||
val totalRelays = relaysToProcess.size
|
||||
|
||||
val outboxTargets = outboxTargets()
|
||||
val inboxTargets = inboxTargets()
|
||||
val dmTargets = dmTargets()
|
||||
|
||||
val defaultFilters =
|
||||
buildList {
|
||||
if (outboxTargets.isNotEmpty()) add(Filter(authors = listOf(myPubKey)))
|
||||
if (inboxTargets.isNotEmpty() || dmTargets.isNotEmpty()) {
|
||||
add(Filter(tags = mapOf("p" to listOf(myPubKey))))
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultFilters.isEmpty()) {
|
||||
_syncState.value = SyncState.Error("No outbox, inbox, or DM relays configured.")
|
||||
return
|
||||
}
|
||||
|
||||
val usersRelays = outboxTargets + inboxTargets + dmTargets
|
||||
|
||||
val perRelayFilters =
|
||||
relaysToProcess.associateWith {
|
||||
if (it !in usersRelays) {
|
||||
defaultFilters
|
||||
} else {
|
||||
buildList {
|
||||
if (it !in outboxTargets) add(Filter(authors = listOf(myPubKey)))
|
||||
if (it !in inboxTargets && it !in dmTargets) {
|
||||
add(Filter(tags = mapOf("p" to listOf(myPubKey))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emitLiveSnapshot(
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
outboxTargets,
|
||||
inboxTargets,
|
||||
dmTargets,
|
||||
)
|
||||
|
||||
// Thread-safe dedup sets — prevent the same event from being forwarded twice when
|
||||
// multiple source relays return the same event concurrently.
|
||||
val outboxDedup = ConcurrentHashMap.newKeySet<String>()
|
||||
val inboxDedup = ConcurrentHashMap.newKeySet<String>()
|
||||
val dmDedup = ConcurrentHashMap.newKeySet<String>()
|
||||
|
||||
val sourceRelayOfEvent = ConcurrentHashMap<HexKey, NormalizedRelayUrl>()
|
||||
|
||||
val runningState =
|
||||
SyncState.Running(
|
||||
relaysCompleted = 0,
|
||||
totalRelays = totalRelays,
|
||||
eventsSent = 0,
|
||||
eventsReceived = 0,
|
||||
eventsAccepted = 0,
|
||||
)
|
||||
|
||||
val okListener =
|
||||
object : IRelayClientListener {
|
||||
override fun onCannotConnect(
|
||||
relay: IRelayClient,
|
||||
errorMessage: String,
|
||||
) {
|
||||
super.onCannotConnect(relay, errorMessage)
|
||||
val currentStatus = liveActivity.value.runningRelays[relay.url]?.status
|
||||
if (currentStatus?.value !is LiveSyncActivity.ConnectionStatus.Error) {
|
||||
currentStatus?.tryEmit(LiveSyncActivity.ConnectionStatus.Error(errorMessage))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSent(
|
||||
relay: IRelayClient,
|
||||
cmdStr: String,
|
||||
cmd: Command,
|
||||
success: Boolean,
|
||||
) {
|
||||
super.onSent(relay, cmdStr, cmd, success)
|
||||
if (cmd is EventCmd) {
|
||||
var hasSent = false
|
||||
|
||||
if (outboxDedup.contains(cmd.event.id)) {
|
||||
liveActivity.value.outboxTargets[relay.url]
|
||||
?.eventsSent
|
||||
?.update { it + 1 }
|
||||
hasSent = true
|
||||
}
|
||||
if (inboxDedup.contains(cmd.event.id)) {
|
||||
liveActivity.value.inboxTargets[relay.url]
|
||||
?.eventsSent
|
||||
?.update { it + 1 }
|
||||
hasSent = true
|
||||
}
|
||||
if (dmDedup.contains(cmd.event.id)) {
|
||||
liveActivity.value.dmTargets[relay.url]
|
||||
?.eventsSent
|
||||
?.update { it + 1 }
|
||||
hasSent = true
|
||||
}
|
||||
|
||||
if (hasSent) {
|
||||
runningState.eventsSent.update { it + 1 }
|
||||
}
|
||||
} else if (cmd is ReqCmd) {
|
||||
val currentStatus = liveActivity.value.runningRelays[relay.url]?.status
|
||||
if (currentStatus?.value != LiveSyncActivity.ConnectionStatus.Querying) {
|
||||
currentStatus?.tryEmit(LiveSyncActivity.ConnectionStatus.Querying)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIncomingMessage(
|
||||
relay: IRelayClient,
|
||||
msgStr: String,
|
||||
msg: Message,
|
||||
) {
|
||||
if (msg is OkMessage && msg.success && msg.message.isBlank()) {
|
||||
// remove() is atomic: returns non-null only for the first OK per event.
|
||||
val sourceRelay = sourceRelayOfEvent.remove(msg.eventId)
|
||||
if (sourceRelay != null) {
|
||||
liveActivity.value.runningRelays[sourceRelay]
|
||||
?.eventsAccepted
|
||||
?.update { it + 1 }
|
||||
}
|
||||
|
||||
if (outboxDedup.contains(msg.eventId)) {
|
||||
val relayTarget = liveActivity.value.outboxTargets[relay.url]
|
||||
if (relayTarget != null) {
|
||||
relayTarget.eventsAccepted.update { it + 1 }
|
||||
runningState.eventsAccepted.update { it + 1 }
|
||||
}
|
||||
}
|
||||
if (dmDedup.contains(msg.eventId)) {
|
||||
val relayTarget = liveActivity.value.dmTargets[relay.url]
|
||||
if (relayTarget != null) {
|
||||
relayTarget.eventsAccepted.update { it + 1 }
|
||||
runningState.eventsAccepted.update { it + 1 }
|
||||
}
|
||||
}
|
||||
if (inboxDedup.contains(msg.eventId)) {
|
||||
val relayTarget = liveActivity.value.inboxTargets[relay.url]
|
||||
if (relayTarget != null) {
|
||||
relayTarget.eventsAccepted.update { it + 1 }
|
||||
runningState.eventsAccepted.update { it + 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_syncState.emit(runningState)
|
||||
|
||||
clientBuilder().use { client ->
|
||||
client.subscribe(okListener)
|
||||
try {
|
||||
client.downloadFromPool(
|
||||
relays = relaysToProcess,
|
||||
filters = perRelayFilters,
|
||||
onNewPage = { until, sourceRelay ->
|
||||
_liveActivity.value.runningRelays[sourceRelay]
|
||||
?.pageUntil
|
||||
?.tryEmit(until)
|
||||
},
|
||||
onEvent = { event, sourceRelay ->
|
||||
val isMyEvent = event.pubKey == myPubKey
|
||||
val mentionsMe = event.tags.isTaggedUser(myPubKey)
|
||||
val isDmKind = event.kind == 4 || event.kind == 1059
|
||||
|
||||
val live = liveActivity.value
|
||||
|
||||
var newEvent = false
|
||||
var matchesAtLeastOneFilter = false
|
||||
|
||||
// Each routing rule is independent: an event can match more than one.
|
||||
if (isMyEvent && outboxTargets.isNotEmpty()) {
|
||||
if (outboxDedup.add(event.id)) {
|
||||
client.send(event, outboxTargets)
|
||||
newEvent = true
|
||||
}
|
||||
matchesAtLeastOneFilter = true
|
||||
}
|
||||
if (mentionsMe && isDmKind && dmTargets.isNotEmpty()) {
|
||||
if (dmDedup.add(event.id)) {
|
||||
client.send(event, dmTargets)
|
||||
newEvent = true
|
||||
}
|
||||
matchesAtLeastOneFilter = true
|
||||
}
|
||||
if (mentionsMe && !isDmKind && inboxTargets.isNotEmpty()) {
|
||||
if (inboxDedup.add(event.id)) {
|
||||
client.send(event, inboxTargets)
|
||||
newEvent = true
|
||||
}
|
||||
matchesAtLeastOneFilter = true
|
||||
}
|
||||
|
||||
if (newEvent) {
|
||||
sourceRelayOfEvent[event.id] = sourceRelay
|
||||
}
|
||||
|
||||
if (matchesAtLeastOneFilter) {
|
||||
runningState.eventsReceived.update { it + 1 }
|
||||
|
||||
live.runningRelays[sourceRelay]?.eventsFound?.update { it + 1 }
|
||||
live.completedRelays[sourceRelay]?.eventsFound?.update { it + 1 }
|
||||
}
|
||||
},
|
||||
onRelayStart = { relay ->
|
||||
_liveActivity.update {
|
||||
it.copy(
|
||||
runningRelays =
|
||||
it.runningRelays +
|
||||
Pair(relay, SourceRelayInfo(relay, LiveSyncActivity.ConnectionStatus.Connecting, 0, 0)),
|
||||
)
|
||||
}
|
||||
},
|
||||
onRelayComplete = { relay ->
|
||||
_liveActivity.update {
|
||||
val newCompleted = it.runningRelays[relay]
|
||||
it.copy(
|
||||
runningRelays = it.runningRelays.minus(relay),
|
||||
completedRelays =
|
||||
if (newCompleted != null) {
|
||||
it.completedRelays.plus(relay to newCompleted)
|
||||
} else {
|
||||
it.completedRelays
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val status = _liveActivity.value.completedRelays[relay]?.status
|
||||
|
||||
if (status?.value !is LiveSyncActivity.ConnectionStatus.Error) {
|
||||
status?.tryEmit(LiveSyncActivity.ConnectionStatus.Completed)
|
||||
}
|
||||
|
||||
runningState.relaysCompleted.update { it + 1 }
|
||||
},
|
||||
)
|
||||
|
||||
_syncState.value =
|
||||
SyncState.Done(
|
||||
totalEventsReceived = runningState.eventsReceived.value,
|
||||
totalEventsSent = runningState.eventsSent.value,
|
||||
totalEventsAccepted = runningState.eventsAccepted.value,
|
||||
durationMs = System.currentTimeMillis() - startTime,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_syncState.value =
|
||||
SyncState.Done(
|
||||
totalEventsReceived = runningState.eventsReceived.value,
|
||||
totalEventsSent = runningState.eventsSent.value,
|
||||
totalEventsAccepted = runningState.eventsAccepted.value,
|
||||
durationMs = System.currentTimeMillis() - startTime,
|
||||
)
|
||||
|
||||
if (e is CancellationException) throw e
|
||||
_syncState.value = SyncState.Error(e.message ?: "Unknown error")
|
||||
} finally {
|
||||
client.unsubscribe(okListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintains a sliding window of up to [MAX_CONCURRENT_RELAYS] active relay workers.
|
||||
* As soon as one relay finishes (all pages exhausted), the next relay from [relays]
|
||||
* starts immediately — no waiting for an entire batch to drain.
|
||||
*
|
||||
* [onEvent] receives the event and the URL of the relay it came from.
|
||||
*/
|
||||
private suspend fun INostrClient.downloadFromPool(
|
||||
relays: List<NormalizedRelayUrl>,
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
onNewPage: (Long, NormalizedRelayUrl) -> Unit,
|
||||
onEvent: (Event, NormalizedRelayUrl) -> Unit,
|
||||
onRelayStart: (NormalizedRelayUrl) -> Unit,
|
||||
onRelayComplete: (NormalizedRelayUrl) -> Unit,
|
||||
) {
|
||||
val semaphore = Semaphore(MAX_CONCURRENT_RELAYS)
|
||||
supervisorScope {
|
||||
for (relay in relays) {
|
||||
if (!isActive) break
|
||||
semaphore.acquire()
|
||||
launch {
|
||||
try {
|
||||
onRelayStart(relay)
|
||||
filters[relay]?.let { filtersForRelay ->
|
||||
downloadFromRelay(
|
||||
relay = relay,
|
||||
filters = filtersForRelay,
|
||||
onNewPage = { onNewPage(it, relay) },
|
||||
onEvent = { onEvent(it, relay) },
|
||||
)
|
||||
} ?: 0
|
||||
onRelayComplete(relay)
|
||||
} finally {
|
||||
semaphore.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all pages from a single [relay] using paginated `until` cursors.
|
||||
* Delegates to the Quartz [downloadFromRelay] extension.
|
||||
*
|
||||
* @return total number of events received across all pages.
|
||||
*/
|
||||
private suspend fun INostrClient.downloadFromRelay(
|
||||
relay: NormalizedRelayUrl,
|
||||
filters: List<Filter>,
|
||||
onNewPage: (Long) -> Unit,
|
||||
onEvent: (Event) -> Unit,
|
||||
): Int = reqBypassingRelayLimits(relay, filters, RELAY_TIMEOUT_MS, onNewPage, onEvent)
|
||||
}
|
||||
+832
@@ -0,0 +1,832 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.relays.eventsync
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDotNoDay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
@Composable
|
||||
fun EventSyncScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val syncViewModel = accountViewModel.eventSync
|
||||
val isMobileOrMetered by accountViewModel.settings.isMobileOrMeteredConnection.collectAsStateWithLifecycle()
|
||||
val syncState by syncViewModel.syncState.collectAsStateWithLifecycle()
|
||||
val liveActivity by syncViewModel.liveActivity.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBarWithBackButton(
|
||||
caption = stringRes(R.string.event_sync_title),
|
||||
popBack = nav::popBack,
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Column(Modifier.fillMaxSize().padding(padding)) {
|
||||
EventScreenBody(
|
||||
syncState = syncState,
|
||||
liveActivity = liveActivity,
|
||||
isMobileOrMetered = isMobileOrMetered,
|
||||
onStart = syncViewModel::start,
|
||||
onCancel = syncViewModel::cancel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EventScreenBody(
|
||||
syncState: EventSync.SyncState,
|
||||
liveActivity: EventSync.LiveSyncActivity,
|
||||
isMobileOrMetered: Boolean = false,
|
||||
onStart: () -> Unit = {},
|
||||
onCancel: () -> Unit = {},
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
item {
|
||||
// ---- Progress / Status area ----
|
||||
when (syncState) {
|
||||
is EventSync.SyncState.Idle -> ExplanationCard(isMobileOrMetered, onStart)
|
||||
is EventSync.SyncState.Running -> SyncProgressCard(state = syncState, onCancel)
|
||||
is EventSync.SyncState.Done -> DoneCard(state = syncState, isMobileOrMetered, onStart)
|
||||
is EventSync.SyncState.Error -> ErrorCard(syncState.message, isMobileOrMetered, onStart)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Live relay activity (shown during and after sync) ----
|
||||
if (liveActivity.outboxTargets.isNotEmpty() ||
|
||||
liveActivity.inboxTargets.isNotEmpty() ||
|
||||
liveActivity.dmTargets.isNotEmpty()
|
||||
) {
|
||||
item {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
DestinationRelaysCard(activity = liveActivity)
|
||||
}
|
||||
}
|
||||
|
||||
val runningSize = liveActivity.runningRelays.size
|
||||
if (runningSize > 0) {
|
||||
item {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_activity_log, runningSize),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(Modifier.height(5.dp))
|
||||
}
|
||||
|
||||
itemsIndexed(liveActivity.runningRelays.values.toList(), key = { _, item -> item.relay.url }) { index, info ->
|
||||
if (index > 0) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
ActivityLogRow(info = info)
|
||||
}
|
||||
}
|
||||
|
||||
val completedSize = liveActivity.completedRelays.size
|
||||
if (completedSize > 0) {
|
||||
item {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_activity_log_finished, completedSize),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(Modifier.height(5.dp))
|
||||
}
|
||||
|
||||
itemsIndexed(liveActivity.sortedCompletedRelays, key = { _, item -> item.relay.url }) { index, info ->
|
||||
if (index > 0) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
ActivityLogRow(info = info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartSyncButton(
|
||||
isMobileOrMetered: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
var showMobileDataDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (isMobileOrMetered) {
|
||||
showMobileDataDialog = true
|
||||
} else {
|
||||
onClick()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringRes(R.string.event_sync_start))
|
||||
}
|
||||
|
||||
// ---- Mobile-data confirmation dialog ----
|
||||
if (showMobileDataDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showMobileDataDialog = false },
|
||||
title = { Text(stringRes(R.string.event_sync_mobile_data_dialog_title)) },
|
||||
text = { Text(stringRes(R.string.event_sync_wifi_warning)) },
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
showMobileDataDialog = false
|
||||
onClick()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(R.string.event_sync_start_anyway))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showMobileDataDialog = false }) {
|
||||
Text(stringRes(R.string.event_sync_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExplanationCard(
|
||||
isMobileOrMetered: Boolean,
|
||||
onStart: () -> Unit,
|
||||
) {
|
||||
// ---- Explanation card ----
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_what_happens_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_what_happens_body),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
StepRow(number = "1", text = stringRes(R.string.event_sync_step1))
|
||||
Spacer(Modifier.height(4.dp))
|
||||
StepRow(number = "2", text = stringRes(R.string.event_sync_step2))
|
||||
Spacer(Modifier.height(4.dp))
|
||||
StepRow(number = "3", text = stringRes(R.string.event_sync_step3))
|
||||
Spacer(Modifier.height(10.dp))
|
||||
// ---- WiFi warning ----
|
||||
if (isMobileOrMetered) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_wifi_warning),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
StartSyncButton(isMobileOrMetered = isMobileOrMetered, onStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Progress / status cards
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Composable
|
||||
private fun SyncProgressCard(
|
||||
state: EventSync.SyncState.Running,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
RelayStatement(state)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
EventsReceivedStatement(state)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
OutlinedButton(
|
||||
onClick = onCancel,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.event_sync_cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EventsReceivedStatement(state: EventSync.SyncState.Running) {
|
||||
val eventsReceived by state.eventsReceived.collectAsStateWithLifecycle()
|
||||
val eventsSent by state.eventsSent.collectAsStateWithLifecycle()
|
||||
val eventsAccepted by state.eventsAccepted.collectAsStateWithLifecycle()
|
||||
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_events_sent, eventsAccepted, eventsSent, eventsReceived),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayStatement(state: EventSync.SyncState.Running) {
|
||||
val relaysCompleted by state.relaysCompleted.collectAsStateWithLifecycle()
|
||||
val totalRelays by state.totalRelays.collectAsStateWithLifecycle()
|
||||
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_relays_progress, relaysCompleted, totalRelays),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = {
|
||||
if (totalRelays > 0) {
|
||||
relaysCompleted / totalRelays.toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DoneCard(
|
||||
state: EventSync.SyncState.Done,
|
||||
isMobileOrMetered: Boolean = false,
|
||||
onStart: () -> Unit = { },
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_done_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_done_sent, state.totalEventsSent, state.totalEventsReceived),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_done_accepted, state.totalEventsAccepted),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_done_duration, (state.durationMs / 1000).toInt()),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f),
|
||||
)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
StartSyncButton(isMobileOrMetered, onStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorCard(
|
||||
message: String,
|
||||
isMobileOrMetered: Boolean = false,
|
||||
onStart: () -> Unit = {},
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_error_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
StartSyncButton(isMobileOrMetered, onStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Live activity cards
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Shows where events are being sent: outbox, inbox, and DM relay lists.
|
||||
*/
|
||||
@Composable
|
||||
private fun DestinationRelaysCard(activity: EventSync.LiveSyncActivity) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_sending_to),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
|
||||
if (activity.outboxTargets.isNotEmpty()) {
|
||||
Spacer(Modifier.height(10.dp))
|
||||
DestinationSection(
|
||||
label = stringRes(R.string.event_sync_outbox_relays),
|
||||
relays = activity.outboxTargets.values,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
if (activity.inboxTargets.isNotEmpty()) {
|
||||
Spacer(Modifier.height(10.dp))
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
DestinationSection(
|
||||
label = stringRes(R.string.event_sync_inbox_relays),
|
||||
relays = activity.inboxTargets.values,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
if (activity.dmTargets.isNotEmpty()) {
|
||||
Spacer(Modifier.height(10.dp))
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
DestinationSection(
|
||||
label = stringRes(R.string.event_sync_dm_relays),
|
||||
relays = activity.dmTargets.values,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DestinationSection(
|
||||
label: String,
|
||||
relays: Collection<EventSync.LiveSyncActivity.DestinationRelayInfo>,
|
||||
color: androidx.compose.ui.graphics.Color,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
relays.forEachIndexed { index, info ->
|
||||
if (index > 0) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
DestinationRelayRow(info = info, color = color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DestinationRelayRow(
|
||||
info: EventSync.LiveSyncActivity.DestinationRelayInfo,
|
||||
color: androidx.compose.ui.graphics.Color,
|
||||
) {
|
||||
val eventsSent by info.eventsSent.collectAsStateWithLifecycle()
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color),
|
||||
)
|
||||
Text(
|
||||
text = info.relay.displayHost(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.StartEllipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (eventsSent > 0) {
|
||||
val eventsAccepted by info.eventsAccepted.collectAsStateWithLifecycle()
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_log_sent, formatCount(eventsSent)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(0.3f),
|
||||
textAlign = TextAlign.End,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.StartEllipsis,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_log_new, formatCount(eventsAccepted)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = if (eventsAccepted > 0) FontWeight.SemiBold else FontWeight.Normal,
|
||||
color = if (eventsAccepted > 0) color else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(0.3f),
|
||||
textAlign = TextAlign.End,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.StartEllipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActivityLogRow(info: EventSync.LiveSyncActivity.SourceRelayInfo) {
|
||||
val eventsFound by info.eventsFound.collectAsStateWithLifecycle()
|
||||
val hasEvents = eventsFound > 0
|
||||
val dotColor =
|
||||
if (hasEvents) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
|
||||
}
|
||||
val textColor =
|
||||
if (hasEvents) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(dotColor),
|
||||
)
|
||||
Text(
|
||||
text = info.relay.displayHost(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.StartEllipsis,
|
||||
modifier = Modifier.weight(0.6f),
|
||||
)
|
||||
if (hasEvents) {
|
||||
val context = LocalContext.current
|
||||
val untilPage by info.pageUntil.collectAsStateWithLifecycle()
|
||||
val eventsAccepted by info.eventsAccepted.collectAsStateWithLifecycle()
|
||||
untilPage?.let {
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_less_than_until, timeAgoNoDotNoDay(it, context)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = textColor,
|
||||
modifier = Modifier.weight(0.3f),
|
||||
maxLines = 1,
|
||||
textAlign = TextAlign.End,
|
||||
overflow = TextOverflow.StartEllipsis,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_log_recv, formatCount(eventsFound)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = textColor,
|
||||
modifier = Modifier.weight(0.3f),
|
||||
maxLines = 1,
|
||||
textAlign = TextAlign.End,
|
||||
overflow = TextOverflow.StartEllipsis,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.event_sync_log_new, formatCount(eventsAccepted)),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = if (eventsAccepted > 0) FontWeight.SemiBold else FontWeight.Normal,
|
||||
color =
|
||||
if (eventsAccepted > 0) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
maxLines = 1,
|
||||
textAlign = TextAlign.End,
|
||||
overflow = TextOverflow.StartEllipsis,
|
||||
modifier = Modifier.weight(0.3f),
|
||||
)
|
||||
} else {
|
||||
val status by info.status.collectAsStateWithLifecycle()
|
||||
Text(
|
||||
text =
|
||||
when (status) {
|
||||
EventSync.LiveSyncActivity.ConnectionStatus.Connecting -> stringRes(R.string.event_sync_status_connecting)
|
||||
EventSync.LiveSyncActivity.ConnectionStatus.Querying -> stringRes(R.string.event_sync_status_downloading)
|
||||
is EventSync.LiveSyncActivity.ConnectionStatus.Error -> (status as EventSync.LiveSyncActivity.ConnectionStatus.Error).msg.ifBlank { stringRes(R.string.event_sync_status_error) }
|
||||
EventSync.LiveSyncActivity.ConnectionStatus.Completed -> stringRes(R.string.event_sync_status_completed)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.End,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.StartEllipsis,
|
||||
modifier = Modifier.weight(0.45f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Composable
|
||||
private fun StepRow(
|
||||
number: String,
|
||||
text: String,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Text(
|
||||
text = "$number.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Strips the WebSocket scheme and trailing slash for compact display. */
|
||||
private fun NormalizedRelayUrl.displayHost(): String =
|
||||
url
|
||||
.removePrefix("wss://")
|
||||
.removePrefix("ws://")
|
||||
.trimEnd('/')
|
||||
|
||||
/** Formats a count with K/M suffix for large numbers. */
|
||||
private fun formatCount(n: Int): String =
|
||||
when {
|
||||
n >= 1_000_000 -> "${n / 1_000}K"
|
||||
else -> n.toString()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Preview data
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private val previewRunning =
|
||||
listOf(
|
||||
EventSync.LiveSyncActivity.SourceRelayInfo(NormalizedRelayUrl("wss://relay.damus.io"), EventSync.LiveSyncActivity.ConnectionStatus.Querying, 1247, 891),
|
||||
EventSync.LiveSyncActivity.SourceRelayInfo(NormalizedRelayUrl("wss://nos.lol"), EventSync.LiveSyncActivity.ConnectionStatus.Querying, 892, 45),
|
||||
EventSync.LiveSyncActivity.SourceRelayInfo(NormalizedRelayUrl("wss://nos2.lol"), EventSync.LiveSyncActivity.ConnectionStatus.Connecting, 0, 0),
|
||||
)
|
||||
|
||||
private val previewCompletions =
|
||||
listOf(
|
||||
EventSync.LiveSyncActivity.SourceRelayInfo(NormalizedRelayUrl("wss://relay.nostr.band"), EventSync.LiveSyncActivity.ConnectionStatus.Completed, 3500, 3498),
|
||||
EventSync.LiveSyncActivity.SourceRelayInfo(NormalizedRelayUrl("wss://slow.relay.example.com"), EventSync.LiveSyncActivity.ConnectionStatus.Completed, 0, 0),
|
||||
EventSync.LiveSyncActivity.SourceRelayInfo(NormalizedRelayUrl("wss://nostr.bitcoiner.social"), EventSync.LiveSyncActivity.ConnectionStatus.Completed, 15, 0),
|
||||
EventSync.LiveSyncActivity.SourceRelayInfo(NormalizedRelayUrl("wss://unreachable.relay.xyz"), EventSync.LiveSyncActivity.ConnectionStatus.Error("connection failed"), 0, 0),
|
||||
)
|
||||
|
||||
private val previewActivity =
|
||||
EventSync.LiveSyncActivity(
|
||||
runningRelays = previewRunning,
|
||||
completedRelays = previewCompletions,
|
||||
outboxTargets =
|
||||
listOf(
|
||||
EventSync.LiveSyncActivity.DestinationRelayInfo(NormalizedRelayUrl("wss://outbox.nostr.com"), 1247, 891),
|
||||
EventSync.LiveSyncActivity.DestinationRelayInfo(NormalizedRelayUrl("wss://relay.damus.io"), 892, 45),
|
||||
),
|
||||
inboxTargets =
|
||||
listOf(
|
||||
EventSync.LiveSyncActivity.DestinationRelayInfo(NormalizedRelayUrl("wss://inbox.nostr.com"), 500, 500),
|
||||
EventSync.LiveSyncActivity.DestinationRelayInfo(NormalizedRelayUrl("wss://nos.lol"), 0, 0),
|
||||
),
|
||||
dmTargets =
|
||||
listOf(
|
||||
EventSync.LiveSyncActivity.DestinationRelayInfo(NormalizedRelayUrl("wss://dm.nostr.com"), 15, 10),
|
||||
),
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Previews
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun IdleCardWifiPreview() {
|
||||
ThemeComparisonColumn {
|
||||
ExplanationCard(false, {})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun IdleCardMobilePreview() {
|
||||
ThemeComparisonColumn {
|
||||
ExplanationCard(true, {})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun SyncProgressCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
SyncProgressCard(
|
||||
state =
|
||||
EventSync.SyncState.Running(
|
||||
relaysCompleted = 312,
|
||||
totalRelays = 1024,
|
||||
eventsAccepted = 12,
|
||||
eventsSent = 4821,
|
||||
eventsReceived = 10000,
|
||||
),
|
||||
onCancel = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun DoneCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
DoneCard(
|
||||
state =
|
||||
EventSync.SyncState.Done(
|
||||
totalEventsReceived = 20_000,
|
||||
totalEventsSent = 18_432,
|
||||
totalEventsAccepted = 14_891,
|
||||
durationMs = 187_000,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun ErrorCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
ErrorCard(message = "No outbox, inbox, or DM relays configured.")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun DestinationRelaysCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
DestinationRelaysCard(activity = previewActivity)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(device = "spec:width=1800px,height=2340px,dpi=440")
|
||||
fun EventScreenBodyPreview() {
|
||||
ThemeComparisonRow {
|
||||
EventScreenBody(
|
||||
EventSync.SyncState.Idle,
|
||||
EventSync.LiveSyncActivity(emptyList(), emptyList(), emptyList(), emptyList(), emptyList()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(device = "spec:width=1800px,height=2340px,dpi=440")
|
||||
fun EventScreenBody2Preview() {
|
||||
ThemeComparisonRow {
|
||||
EventScreenBody(
|
||||
EventSync.SyncState.Running(1047, 1224, 100, 4821, 10000),
|
||||
previewActivity,
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -67,8 +67,8 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransaction
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransactionType
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransactionType
|
||||
import java.text.NumberFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
+13
-13
@@ -23,19 +23,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransaction
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@@ -1811,10 +1811,52 @@
|
||||
<string name="select_all">Select All</string>
|
||||
<string name="uptime">%1$d%% uptime</string>
|
||||
<string name="namecoin_settings">Namecoin Settings</string>
|
||||
<string name="event_sync_title">Relay Sync</string>
|
||||
<string name="event_sync_section">Relay Sync</string>
|
||||
<string name="event_sync_section_explainer">Re-publish your events across all known relays to keep your outbox, inbox, and DM relays up to date. Requires Wi-Fi — this may use a lot of data.</string>
|
||||
<string name="event_sync_open_button">Open Relay Sync…</string>
|
||||
<string name="event_sync_what_happens_title">What this does</string>
|
||||
<string name="event_sync_what_happens_body">This tool scans every relay your app has seen and redistributes your events to the correct destinations: </string>
|
||||
<string name="event_sync_step1">Download all events you authored and send them to your outbox relays.</string>
|
||||
<string name="event_sync_step2">Download all events that mention you and send them to your inbox relays.</string>
|
||||
<string name="event_sync_step3">Download all direct messages addressed to you and send them to your DM relays.</string>
|
||||
<string name="event_sync_wifi_warning">⚠ You appear to be on a metered or mobile connection. This operation can transfer a very large amount of data. Connect to Wi-Fi before starting.</string>
|
||||
<string name="event_sync_mobile_data_dialog_title">Use Mobile Data?</string>
|
||||
<string name="event_sync_start">Start Sync</string>
|
||||
<string name="event_sync_start_anyway">Start Anyway (mobile data)</string>
|
||||
<string name="event_sync_pause">Pause</string>
|
||||
<string name="event_sync_resume">Resume</string>
|
||||
<string name="event_sync_start_over">Start Over</string>
|
||||
<string name="event_sync_cancel">Cancel</string>
|
||||
<string name="event_sync_relays_progress">Relays: %1$d / %2$d</string>
|
||||
<string name="event_sync_events_sent">Events redistributed: %1$d new out of %2$d sent and %3$d received</string>
|
||||
<string name="event_sync_paused_title">Sync Paused</string>
|
||||
<string name="event_sync_paused_body">Completed %1$d of %2$d relays — %3$d events redistributed so far. Tap Resume to continue.</string>
|
||||
<string name="event_sync_done_title">Sync complete</string>
|
||||
<string name="event_sync_done_sent">Forwarded %1$d events to destination relays out of %2$d received.</string>
|
||||
<string name="event_sync_done_accepted">%1$d events accepted as new by destination relays.</string>
|
||||
<string name="event_sync_done_duration">Completed in %1$d seconds.</string>
|
||||
<string name="event_sync_error_title">Sync error</string>
|
||||
<string name="event_sync_sending_to">Sending To</string>
|
||||
<string name="event_sync_outbox_relays">Outbox</string>
|
||||
<string name="event_sync_inbox_relays">Inbox</string>
|
||||
<string name="event_sync_dm_relays">DMs</string>
|
||||
<string name="event_sync_activity_log">Currently Checking (%1$d relays)</string>
|
||||
<string name="event_sync_activity_log_finished">Finished (%1$d relays)</string>
|
||||
<string name="event_sync_log_sent">sent %1$s</string>
|
||||
<string name="event_sync_log_recv">recv %1$s</string>
|
||||
<string name="event_sync_log_new">new %1$s</string>
|
||||
<string name="event_sync_no_events">no events</string>
|
||||
<string name="ots_explorer_settings">Bitcoin Explorer (OTS)</string>
|
||||
<string name="events">events</string>
|
||||
<string name="dms">DMs</string>
|
||||
<string name="profiles">profiles</string>
|
||||
<string name="relay_settings_lower">relay settings</string>
|
||||
<string name="last_seen">Last seen %1$s ago</string>
|
||||
|
||||
<string name="event_sync_less_than_until"><%1$s</string>
|
||||
<string name="event_sync_status_connecting">Connecting</string>
|
||||
<string name="event_sync_status_downloading">Downloading</string>
|
||||
<string name="event_sync_status_error">Error</string>
|
||||
<string name="event_sync_status_completed">Completed</string>
|
||||
</resources>
|
||||
|
||||
@@ -26,10 +26,10 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip57Zaps.IPrivateZapsDecryptionCache
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
|
||||
@@ -48,10 +48,10 @@ import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
|
||||
import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.commons.services.nwc
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+2
-2
@@ -33,8 +33,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
|
||||
+4
-4
@@ -35,10 +35,10 @@ import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip57Zaps.IPrivateZapsDecryptionCache
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
|
||||
+5
-5
@@ -27,12 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
+3
@@ -25,6 +25,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.test.Test
|
||||
|
||||
+3
-3
@@ -33,12 +33,12 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization.BunkerMessageKSerializer
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization.BunkerRequestKSerializer
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization.BunkerResponseKSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization.Nip47NotificationKSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization.Nip47RequestKSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization.Nip47ResponseKSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.kotlinSerialization.RumorKSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
+3
-1
@@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface INostrClient {
|
||||
interface INostrClient : AutoCloseable {
|
||||
fun connectedRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
|
||||
|
||||
fun availableRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
|
||||
@@ -136,4 +136,6 @@ object EmptyNostrClient : INostrClient {
|
||||
override fun activeCounts(url: NormalizedRelayUrl): Map<String, List<Filter>> = emptyMap()
|
||||
|
||||
override fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey> = emptySet()
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
|
||||
+6
-1
@@ -79,7 +79,8 @@ class NostrClient(
|
||||
private val websocketBuilder: WebsocketBuilder,
|
||||
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
) : INostrClient,
|
||||
IRelayClientListener {
|
||||
IRelayClientListener,
|
||||
AutoCloseable {
|
||||
private val relayPool: RelayPool = RelayPool(websocketBuilder, this)
|
||||
|
||||
private val activeRequests: PoolRequests = PoolRequests()
|
||||
@@ -326,4 +327,8 @@ class NostrClient(
|
||||
override fun connectedRelaysFlow() = relayPool.connectedRelays
|
||||
|
||||
override fun availableRelaysFlow() = relayPool.availableRelays
|
||||
|
||||
override fun close() {
|
||||
disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.quartz.nip01Core.relay.client.accessories
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient.close
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient.openReqSubscription
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
/**
|
||||
* Downloads all pages of events matching [filters] from a single [relay] using
|
||||
* paginated `until` cursors.
|
||||
*
|
||||
* After EOSE the oldest [Event.createdAt] seen in that page minus one becomes the
|
||||
* next `until`, and the query repeats until the relay returns no new events.
|
||||
*
|
||||
* Event counting is tracked per filter using [Filter.match]. A filter is considered
|
||||
* fulfilled when the number of matching events reaches its [Filter.limit]. Pagination
|
||||
* stops when all filters with limits are fulfilled or when a page returns no events.
|
||||
* Filters without a limit are considered unbounded and only stop on empty pages.
|
||||
*
|
||||
* @param relay The relay to query.
|
||||
* @param filters Filters to apply on every page (the `until` field is overwritten per page).
|
||||
* @param timeoutMs Maximum time to wait for a single page's EOSE before giving up.
|
||||
* @param onEvent Called for every event received (in page order, after each EOSE).
|
||||
* @return Total number of events received across all pages.
|
||||
*/
|
||||
suspend fun INostrClient.reqBypassingRelayLimits(
|
||||
relay: NormalizedRelayUrl,
|
||||
filters: List<Filter>,
|
||||
timeoutMs: Long = 30_000L,
|
||||
onNewPage: ((Long) -> Unit)? = null,
|
||||
onEvent: (Event) -> Unit,
|
||||
): Int {
|
||||
var until: Long? = null
|
||||
var totalEvents = 0
|
||||
|
||||
// Track how many matching events each filter has received so far.
|
||||
val matchCountPerFilter = IntArray(filters.size)
|
||||
|
||||
val subId = newSubId()
|
||||
|
||||
while (true) {
|
||||
coroutineContext.ensureActive()
|
||||
|
||||
// Only include filters that still need more events.
|
||||
val remainingFilters =
|
||||
filters.filterIndexed { index, filter ->
|
||||
val limit = filter.limit
|
||||
limit == null || matchCountPerFilter[index] < limit
|
||||
}
|
||||
|
||||
if (remainingFilters.isEmpty()) break
|
||||
|
||||
val doneChannel = Channel<Unit>(Channel.CONFLATED)
|
||||
|
||||
val activeFilters =
|
||||
if (until == null) {
|
||||
remainingFilters
|
||||
} else {
|
||||
onNewPage?.invoke(until)
|
||||
remainingFilters.map { it.copy(until = until) }
|
||||
}
|
||||
|
||||
var pageCount = 0
|
||||
var pageMinTs = Long.MAX_VALUE
|
||||
|
||||
val listener =
|
||||
object : IRequestListener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
isLive: Boolean,
|
||||
relayInner: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
onEvent(event)
|
||||
pageCount++
|
||||
if (event.createdAt < pageMinTs) pageMinTs = event.createdAt
|
||||
|
||||
// Count this event against every base filter it matches.
|
||||
if (matchCountPerFilter.size == 1) {
|
||||
// no need to run the match.
|
||||
matchCountPerFilter[0]++
|
||||
} else {
|
||||
for (i in filters.indices) {
|
||||
val limit = filters[i].limit
|
||||
if ((limit == null || matchCountPerFilter[i] < limit) && filters[i].match(event)) {
|
||||
matchCountPerFilter[i]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relayInner: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
doneChannel.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onClosed(
|
||||
message: String,
|
||||
relayInner: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
doneChannel.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onCannotConnect(
|
||||
relayInner: NormalizedRelayUrl,
|
||||
message: String,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
doneChannel.trySend(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
openReqSubscription(subId, mapOf(relay to activeFilters), listener)
|
||||
|
||||
withTimeoutOrNull(timeoutMs) {
|
||||
doneChannel.receive()
|
||||
}
|
||||
|
||||
close(subId)
|
||||
doneChannel.close()
|
||||
|
||||
if (pageCount == 0) break
|
||||
|
||||
totalEvents += pageCount
|
||||
|
||||
// Advance cursor: next page starts just before the oldest event seen.
|
||||
until = pageMinTs - 1
|
||||
}
|
||||
|
||||
return totalEvents
|
||||
}
|
||||
|
||||
suspend fun INostrClient.reqBypassingRelayLimits(
|
||||
relay: String,
|
||||
filters: List<Filter>,
|
||||
timeoutMs: Long = 30_000L,
|
||||
onNewPage: ((Long) -> Unit)? = null,
|
||||
onEvent: (Event) -> Unit,
|
||||
): Int =
|
||||
reqBypassingRelayLimits(
|
||||
relay = RelayUrlNormalizer.normalize(relay),
|
||||
filters = filters,
|
||||
timeoutMs = timeoutMs,
|
||||
onNewPage = onNewPage,
|
||||
onEvent = onEvent,
|
||||
)
|
||||
+24
-1
@@ -67,7 +67,7 @@ suspend fun INostrClient.downloadFirstEvent(
|
||||
subscriptionId: String = newSubId(),
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
): Event? {
|
||||
val resultChannel = Channel<Event>(UNLIMITED)
|
||||
val resultChannel = Channel<Event?>(UNLIMITED)
|
||||
|
||||
val listener =
|
||||
object : IRequestListener {
|
||||
@@ -79,6 +79,29 @@ suspend fun INostrClient.downloadFirstEvent(
|
||||
) {
|
||||
resultChannel.trySend(event)
|
||||
}
|
||||
|
||||
override fun onCannotConnect(
|
||||
relay: NormalizedRelayUrl,
|
||||
message: String,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
resultChannel.trySend(null)
|
||||
}
|
||||
|
||||
override fun onClosed(
|
||||
message: String,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
resultChannel.trySend(null)
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
resultChannel.trySend(null)
|
||||
}
|
||||
}
|
||||
|
||||
openReqSubscription(subscriptionId, filters, listener)
|
||||
|
||||
+55
-15
@@ -28,26 +28,63 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlin.compareTo
|
||||
|
||||
class PoolEventOutbox {
|
||||
private var eventOutbox = mapOf<HexKey, PoolEventOutboxState>()
|
||||
val relays = MutableStateFlow(setOf<NormalizedRelayUrl>())
|
||||
|
||||
fun updateRelays() {
|
||||
val myRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
eventOutbox.values.forEach {
|
||||
myRelays.addAll(it.relaysLeft())
|
||||
fun needsToUpdateRelays(): Boolean {
|
||||
val currentRelays = relays.value
|
||||
|
||||
var relaysToRemoveCounter = 0
|
||||
|
||||
currentRelays.forEach { currentRelay ->
|
||||
if (eventOutbox.values.none { currentRelay in it.relaysRemaining }) {
|
||||
relaysToRemoveCounter++
|
||||
}
|
||||
}
|
||||
|
||||
if (relays.value != myRelays) {
|
||||
relays.tryEmit(myRelays)
|
||||
var relaysToAddCounter = 0
|
||||
eventOutbox.values.forEach { outboxState ->
|
||||
if (outboxState.relaysRemaining.any { it !in currentRelays }) {
|
||||
relaysToAddCounter++
|
||||
}
|
||||
}
|
||||
|
||||
return relaysToRemoveCounter > 0 || relaysToAddCounter > 0
|
||||
}
|
||||
|
||||
fun updateRelays() {
|
||||
if (needsToUpdateRelays()) {
|
||||
relays.update { currentRelays ->
|
||||
val relaysToRemove = mutableSetOf<NormalizedRelayUrl>()
|
||||
|
||||
currentRelays.forEach { currentRelay ->
|
||||
if (eventOutbox.values.none { currentRelay in it.relaysRemaining }) {
|
||||
relaysToRemove.add(currentRelay)
|
||||
}
|
||||
}
|
||||
|
||||
val relaysToAdd = mutableSetOf<NormalizedRelayUrl>()
|
||||
eventOutbox.values.forEach { outboxState ->
|
||||
outboxState.relaysRemaining.forEach { relay ->
|
||||
if (relay !in relaysToAdd && relay !in currentRelays) {
|
||||
relaysToAdd.add(relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(currentRelays - relaysToRemove) + relaysToAdd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun activeOutboxCacheFor(url: NormalizedRelayUrl): Set<HexKey> {
|
||||
val myEvents = mutableSetOf<HexKey>()
|
||||
eventOutbox.forEach { (eventId, outboxCache) ->
|
||||
if (url in outboxCache.relays) {
|
||||
if (url in outboxCache.relaysRemaining) {
|
||||
myEvents.add(eventId)
|
||||
}
|
||||
}
|
||||
@@ -72,7 +109,12 @@ class PoolEventOutbox {
|
||||
id: HexKey,
|
||||
url: NormalizedRelayUrl,
|
||||
) {
|
||||
eventOutbox[id]?.newTry(url)
|
||||
val waiting = eventOutbox[id]
|
||||
waiting?.newTry(url)
|
||||
if (waiting?.isDone() == true) {
|
||||
eventOutbox = eventOutbox - waiting.event.id
|
||||
updateRelays()
|
||||
}
|
||||
}
|
||||
|
||||
fun newResponse(
|
||||
@@ -84,15 +126,13 @@ class PoolEventOutbox {
|
||||
val waiting = eventOutbox[id]
|
||||
if (waiting != null) {
|
||||
waiting.newResponse(url, success, message)
|
||||
clear()
|
||||
if (waiting.isDone()) {
|
||||
eventOutbox = eventOutbox - waiting.event.id
|
||||
updateRelays()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
eventOutbox = eventOutbox.filter { !it.value.isDone() }
|
||||
updateRelays()
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// State management functions
|
||||
// --------------------------
|
||||
@@ -143,7 +183,7 @@ class PoolEventOutbox {
|
||||
errorMessage: String,
|
||||
) {
|
||||
eventOutbox.forEach {
|
||||
if (relay in it.value.relays) {
|
||||
if (relay in it.value.relaysRemaining) {
|
||||
newResponse(it.key, relay, false, errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
+38
-30
@@ -26,35 +26,37 @@ import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class PoolEventOutboxState(
|
||||
val event: Event,
|
||||
var relays: Set<NormalizedRelayUrl>,
|
||||
var relaysRemaining: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
private var tries = mapOf<NormalizedRelayUrl, Tries>()
|
||||
private var failures = mapOf<NormalizedRelayUrl, Tries>()
|
||||
|
||||
fun updateRelays(newRelays: Set<NormalizedRelayUrl>) {
|
||||
relays = newRelays
|
||||
relaysRemaining = newRelays
|
||||
}
|
||||
|
||||
fun isDone(url: NormalizedRelayUrl) = tries[url]?.isDone() ?: false
|
||||
fun isDone() = relaysRemaining.isEmpty()
|
||||
|
||||
fun isDone() = relays.all { isDone(it) }
|
||||
fun relaysLeft(): Set<NormalizedRelayUrl> = relaysRemaining
|
||||
|
||||
fun relaysLeft(): Set<NormalizedRelayUrl> = relays.filterTo(mutableSetOf()) { !isDone(it) }
|
||||
|
||||
fun isSupposedToGo(url: NormalizedRelayUrl) = url in relays && !isDone(url)
|
||||
fun isSupposedToGo(url: NormalizedRelayUrl) = url in relaysRemaining
|
||||
|
||||
fun forEachUnsentEvent(
|
||||
url: NormalizedRelayUrl,
|
||||
run: (url: Event) -> Unit,
|
||||
) = if (isSupposedToGo(url)) run(event) else null
|
||||
|
||||
fun remainingRelays() = relays.filterTo(mutableSetOf(), ::isSupposedToGo)
|
||||
fun remainingRelays() = relaysRemaining
|
||||
|
||||
fun newTry(url: NormalizedRelayUrl) {
|
||||
val currentTries = tries[url]
|
||||
val currentTries = failures[url]
|
||||
if (currentTries != null) {
|
||||
currentTries.addTriedTime(TimeUtils.now())
|
||||
if (currentTries.isDone()) {
|
||||
relaysRemaining = relaysRemaining - url
|
||||
failures = failures - url
|
||||
}
|
||||
} else {
|
||||
tries = tries + (url to Tries(listOf(TimeUtils.now())))
|
||||
failures = failures + (url to Tries(listOf(TimeUtils.now())))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,38 +65,44 @@ class PoolEventOutboxState(
|
||||
success: Boolean,
|
||||
message: String,
|
||||
) {
|
||||
val currentTries = tries[url]
|
||||
if (currentTries != null) {
|
||||
currentTries.addResponse(Response(success, message))
|
||||
val currentTries = failures[url]
|
||||
if (success || message.shouldDiscard()) {
|
||||
relaysRemaining = relaysRemaining - url
|
||||
failures = failures - url
|
||||
} else {
|
||||
tries = tries + (
|
||||
url to
|
||||
Tries(
|
||||
listOf(TimeUtils.now() - 1),
|
||||
listOf(Response(success, message)),
|
||||
)
|
||||
)
|
||||
if (currentTries != null) {
|
||||
currentTries.addResponse(message)
|
||||
} else {
|
||||
failures = failures + (
|
||||
url to
|
||||
Tries(
|
||||
listOf(TimeUtils.now() - 1),
|
||||
listOf(message),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.shouldDiscard() =
|
||||
this.startsWith("replaced:") ||
|
||||
this.startsWith("pow:") ||
|
||||
this.startsWith("deleted:") ||
|
||||
this.startsWith("invalid:")
|
||||
|
||||
// Tries 3 times
|
||||
class Tries(
|
||||
var tries: List<Long> = listOf(),
|
||||
var responses: List<Response> = listOf(),
|
||||
var responses: List<String> = listOf(),
|
||||
) {
|
||||
fun isDone() = responses.any { it.success } || responses.size > 2 || tries.size > 3
|
||||
fun isDone() = responses.size > 2 || tries.size > 3
|
||||
|
||||
fun addResponse(r: Response) {
|
||||
responses += r
|
||||
fun addResponse(msg: String) {
|
||||
responses += msg
|
||||
}
|
||||
|
||||
fun addTriedTime(tried: Long) {
|
||||
tries += tried
|
||||
}
|
||||
}
|
||||
|
||||
class Response(
|
||||
val success: Boolean,
|
||||
val message: String,
|
||||
)
|
||||
}
|
||||
|
||||
+9
-5
@@ -265,11 +265,15 @@ class PoolRequests {
|
||||
errorMessage: String,
|
||||
) {
|
||||
relayState.forEach { subId, state ->
|
||||
desiredSubListeners.get(subId)?.onCannotConnect(
|
||||
message = errorMessage,
|
||||
relay = url,
|
||||
forFilters = state.lastKnownFilterStates(url),
|
||||
)
|
||||
// These are all my subs.. need to figure out which relays have them
|
||||
val subs = desiredSubs.get(subId)
|
||||
if (subs != null && url in subs.keys) {
|
||||
desiredSubListeners.get(subId)?.onCannotConnect(
|
||||
relay = url,
|
||||
message = errorMessage,
|
||||
forFilters = state.lastKnownFilterStates(url),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -42,6 +42,8 @@ class RelayStats(
|
||||
override fun create(key: NormalizedRelayUrl): RelayStat = RelayStat()
|
||||
}
|
||||
|
||||
fun snapshot(): Map<NormalizedRelayUrl, RelayStat> = innerCache.snapshot()
|
||||
|
||||
fun get(url: NormalizedRelayUrl): RelayStat = innerCache[url] ?: throw IllegalArgumentException("Should never happen")
|
||||
|
||||
private val clientListener =
|
||||
|
||||
+20
@@ -27,6 +27,26 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.TlvRecord
|
||||
|
||||
/**
|
||||
* High-level NIP-47 Wallet Connect client.
|
||||
|
||||
+23
@@ -23,6 +23,29 @@ package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcError
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorCode
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentReceivedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentSentNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageSuccessResponse
|
||||
|
||||
/**
|
||||
* High-level NIP-47 Wallet Connect server (wallet service).
|
||||
|
||||
+3
-1
@@ -18,11 +18,13 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.cache
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
|
||||
class NostrWalletConnectRequestCache(
|
||||
signer: NostrSigner,
|
||||
+3
-1
@@ -18,11 +18,13 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.cache
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
|
||||
class NostrWalletConnectResponseCache(
|
||||
signer: NostrSigner,
|
||||
+3
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -27,6 +27,8 @@ import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
+2
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
+2
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip31Alts.AltTag
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Notification
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
+6
-6
@@ -20,12 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization
|
||||
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedData
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcNotificationType
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PaymentReceivedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PaymentSentNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.HoldInvoiceAcceptedData
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.HoldInvoiceAcceptedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcNotificationType
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentReceivedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentSentNotification
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
|
||||
+26
-26
@@ -20,32 +20,32 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization
|
||||
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.TlvRecord
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageParams
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.TlvRecord
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
|
||||
+20
-20
@@ -20,26 +20,26 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization
|
||||
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcError
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorCode
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransaction
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcError
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorCode
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageSuccessResponse
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.rpc
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.rpc
|
||||
|
||||
enum class NwcErrorCode {
|
||||
RATE_LIMITED,
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.rpc
|
||||
|
||||
object NwcMethod {
|
||||
const val PAY_INVOICE = "pay_invoice"
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.rpc
|
||||
|
||||
object NwcTransactionType {
|
||||
const val INCOMING = "incoming"
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.rpc
|
||||
|
||||
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.rpc
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.quartz.nip47WalletConnect
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect.rpc
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable
|
||||
|
||||
@@ -76,10 +76,10 @@ import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcInfoEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcNotificationEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PinListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
|
||||
+2
@@ -101,6 +101,8 @@ private class TrackingNostrClient : INostrClient {
|
||||
override fun activeCounts(url: NormalizedRelayUrl): Map<String, List<Filter>> = emptyMap()
|
||||
|
||||
override fun activeOutboxCache(url: NormalizedRelayUrl): Set<String> = emptySet()
|
||||
|
||||
override fun close() {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+31
@@ -21,6 +21,37 @@
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.HoldInvoiceAcceptedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorCode
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransactionState
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransactionType
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentReceivedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentSentNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageSuccessResponse
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
+4
@@ -24,6 +24,10 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
+4
@@ -21,6 +21,10 @@
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.HoldInvoiceAcceptedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentReceivedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentSentNotification
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
+1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent
|
||||
import com.vitorpamplona.quartz.utils.DeterministicSigner
|
||||
import com.vitorpamplona.quartz.utils.nsecToKeyPair
|
||||
import kotlin.test.Test
|
||||
|
||||
+7
@@ -20,6 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcBudgetRenewal
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcError
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorCode
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcNotificationType
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransactionState
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransactionType
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
+1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcNotificationEvent
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
+16
@@ -21,6 +21,22 @@
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.TlvRecord
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
+17
@@ -21,6 +21,23 @@
|
||||
package com.vitorpamplona.quartz.nip47WalletConnect
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorCode
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageSuccessResponse
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
+3
@@ -24,6 +24,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
+3
-3
@@ -52,15 +52,15 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerRequestDeseriali
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerRequestSerializer
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerResponseDeserializer
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerResponseSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.jackson.NotificationDeserializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.jackson.NotificationSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.jackson.RequestDeserializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.jackson.RequestSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.jackson.ResponseDeserializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.jackson.ResponseSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorDeserializer
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorSerializer
|
||||
|
||||
+5
-5
@@ -24,11 +24,11 @@ import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcNotificationType
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PaymentReceivedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PaymentSentNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.HoldInvoiceAcceptedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcNotificationType
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentReceivedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentSentNotification
|
||||
import com.vitorpamplona.quartz.utils.asTextOrNull
|
||||
|
||||
class NotificationDeserializer : StdDeserializer<Notification>(Notification::class.java) {
|
||||
|
||||
+4
-4
@@ -23,10 +23,10 @@ package com.vitorpamplona.quartz.nip47WalletConnect.jackson
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PaymentReceivedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PaymentSentNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.HoldInvoiceAcceptedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Notification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentReceivedNotification
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaymentSentNotification
|
||||
|
||||
class NotificationSerializer : StdSerializer<Notification>(Notification::class.java) {
|
||||
override fun serialize(
|
||||
|
||||
+15
-15
@@ -24,21 +24,21 @@ import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageMethod
|
||||
import com.vitorpamplona.quartz.utils.asTextOrNull
|
||||
|
||||
class RequestDeserializer : StdDeserializer<Request>(Request::class.java) {
|
||||
|
||||
+11
-11
@@ -23,17 +23,17 @@ package com.vitorpamplona.quartz.nip47WalletConnect.jackson
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageMethod
|
||||
|
||||
class RequestSerializer : StdSerializer<Request>(Request::class.java) {
|
||||
override fun serialize(
|
||||
|
||||
+18
-18
@@ -24,24 +24,24 @@ import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcError
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcError
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageSuccessResponse
|
||||
import com.vitorpamplona.quartz.utils.asTextOrNull
|
||||
|
||||
class ResponseDeserializer : StdDeserializer<Response>(Response::class.java) {
|
||||
|
||||
+16
-16
@@ -23,22 +23,22 @@ package com.vitorpamplona.quartz.nip47WalletConnect.jackson
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CancelHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.CreateConnectionSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBudgetSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.ListTransactionsSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.LookupInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.MakeInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayKeysendSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SettleHoldInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.SignMessageSuccessResponse
|
||||
|
||||
class ResponseSerializer : StdSerializer<Response>(Response::class.java) {
|
||||
override fun serialize(
|
||||
|
||||
+24
-1
@@ -21,11 +21,34 @@
|
||||
package com.vitorpamplona.quartz.nip01Core.relay
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
class DefaultContentTypeInterceptor(
|
||||
private val userAgentHeader: String,
|
||||
) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest: Request = chain.request()
|
||||
val requestWithUserAgent: Request =
|
||||
originalRequest
|
||||
.newBuilder()
|
||||
.header("User-Agent", userAgentHeader)
|
||||
.build()
|
||||
return chain.proceed(requestWithUserAgent)
|
||||
}
|
||||
}
|
||||
|
||||
open class BaseNostrClientTest {
|
||||
companion object {
|
||||
val rootClient = OkHttpClient.Builder().build()
|
||||
val rootClient =
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.addInterceptor(DefaultContentTypeInterceptor("Amethyst/v1.05"))
|
||||
.build()
|
||||
val socketBuilder = BasicOkHttpWebSocket.Builder { url -> rootClient }
|
||||
}
|
||||
}
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.quartz.nip01Core.relay
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.reqBypassingRelayLimits
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class NostrClientReqBypassingRelayLimitsTest : BaseNostrClientTest() {
|
||||
@Test
|
||||
fun testDownloadFromRelayReturnsMetadataEvents() =
|
||||
runBlocking {
|
||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
val client = NostrClient(socketBuilder, appScope)
|
||||
|
||||
val events = mutableListOf<Event>()
|
||||
|
||||
// nos.lol returns only 500 events per req
|
||||
val totalFound =
|
||||
client.reqBypassingRelayLimits(
|
||||
relay = "wss://nos.lol",
|
||||
filters =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
limit = 1000,
|
||||
),
|
||||
),
|
||||
) { event ->
|
||||
events.add(event)
|
||||
}
|
||||
|
||||
client.disconnect()
|
||||
delay(500)
|
||||
appScope.cancel()
|
||||
|
||||
assertEquals(1000, totalFound, "Expected 1000 events from wss://nos.lol")
|
||||
assertEquals(1000, events.size, "Events list should be 1000 events")
|
||||
events.forEach { event ->
|
||||
assertEquals(MetadataEvent.KIND, event.kind, "All events should be kind ${MetadataEvent.KIND}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDownloadFromRelayReturnsMetadataAndContactListEvents() =
|
||||
runBlocking {
|
||||
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
val client = NostrClient(socketBuilder, appScope)
|
||||
|
||||
val metadataEvents = mutableListOf<Event>()
|
||||
val contactListEvents = mutableListOf<Event>()
|
||||
|
||||
// nos.lol returns only 500 events per req
|
||||
val totalFound =
|
||||
client.reqBypassingRelayLimits(
|
||||
relay = "wss://nos.lol",
|
||||
filters =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(MetadataEvent.KIND),
|
||||
limit = 1000,
|
||||
),
|
||||
Filter(
|
||||
kinds = listOf(ContactListEvent.KIND),
|
||||
limit = 1500,
|
||||
),
|
||||
),
|
||||
) { event ->
|
||||
if (event.kind == MetadataEvent.KIND) {
|
||||
metadataEvents.add(event)
|
||||
}
|
||||
if (event.kind == ContactListEvent.KIND) {
|
||||
contactListEvents.add(event)
|
||||
}
|
||||
}
|
||||
|
||||
client.disconnect()
|
||||
delay(500)
|
||||
appScope.cancel()
|
||||
|
||||
assertEquals(2500, totalFound, "Expected 1000 events from wss://nos.lol")
|
||||
assertEquals(1000, metadataEvents.size, "Events list should be 1000 events")
|
||||
assertEquals(1500, contactListEvents.size, "Events list should be 1000 events")
|
||||
metadataEvents.forEach { event ->
|
||||
assertEquals(MetadataEvent.KIND, event.kind, "All events should be kind ${MetadataEvent.KIND}")
|
||||
}
|
||||
contactListEvents.forEach { event ->
|
||||
assertEquals(ContactListEvent.KIND, event.kind, "All events should be kind ${ContactListEvent.KIND}")
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -24,6 +24,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetBalanceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.GetInfoMethod
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
Reference in New Issue
Block a user