Fix issue with wallet loading
Changelog-Changed: Increased transaction list limit to 50 transactions Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -85,8 +85,8 @@ class NostrNetworkManager {
|
||||
self.pool.send_raw_to_local_ndb(.typical(.event(event)))
|
||||
}
|
||||
|
||||
func send(event: NostrEvent) {
|
||||
self.pool.send(.event(event))
|
||||
func send(event: NostrEvent, to targetRelays: [RelayURL]? = nil, skipEphemeralRelays: Bool = true) {
|
||||
self.pool.send(.event(event), to: targetRelays, skip_ephemeral: skipEphemeralRelays)
|
||||
}
|
||||
|
||||
func query(filters: [NostrFilter], to: [RelayURL]? = nil) async -> [NostrEvent] {
|
||||
@@ -208,14 +208,6 @@ class NostrNetworkManager {
|
||||
WalletConnect.pay(url: url, pool: self.pool, post: post, invoice: invoice, zap_request: nil)
|
||||
}
|
||||
|
||||
func requestTransactionList(url: WalletConnectURL, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) {
|
||||
WalletConnect.request_transaction_list(url: url, pool: self.pool, post: self.postbox, delay: delay, on_flush: on_flush)
|
||||
}
|
||||
|
||||
func requestBalanceInformation(url: WalletConnectURL, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) {
|
||||
WalletConnect.request_balance_information(url: url, pool: self.pool, post: self.postbox, delay: delay, on_flush: on_flush)
|
||||
}
|
||||
|
||||
/// Send a donation zap to the Damus team
|
||||
func send_donation_zap(nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
|
||||
let percent_f = Double(percent) / 100.0
|
||||
|
||||
@@ -25,6 +25,28 @@ extension NostrNetworkManager {
|
||||
|
||||
// MARK: - Reading data from Nostr
|
||||
|
||||
/// Subscribes to data from user's relays, for a maximum period of time — after which the stream will end.
|
||||
///
|
||||
/// This is useful when waiting for some specific data from Nostr, but not indefinitely.
|
||||
func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, timeout: Duration) -> AsyncStream<StreamItem> {
|
||||
return AsyncStream<StreamItem> { continuation in
|
||||
let streamingTask = Task {
|
||||
for await item in self.subscribe(filters: filters, to: desiredRelays) {
|
||||
try Task.checkCancellation()
|
||||
continuation.yield(item)
|
||||
}
|
||||
}
|
||||
let timeoutTask = Task {
|
||||
try await Task.sleep(for: timeout)
|
||||
continuation.finish() // End the stream due to timeout.
|
||||
}
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
timeoutTask.cancel()
|
||||
streamingTask.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribes to data from the user's relays
|
||||
///
|
||||
/// ## Implementation notes
|
||||
@@ -112,10 +134,16 @@ extension NostrNetworkManager {
|
||||
}
|
||||
let streamTask = Task {
|
||||
do {
|
||||
for await _ in self.pool.subscribe(filters: filters, to: desiredRelays) {
|
||||
for await item in self.pool.subscribe(filters: filters, to: desiredRelays) {
|
||||
// NO-OP. Notes will be automatically ingested by NostrDB
|
||||
// TODO: Improve efficiency of subscriptions?
|
||||
try Task.checkCancellation()
|
||||
switch item {
|
||||
case .event(let event):
|
||||
Log.debug("Session subscribe: Received kind %d event with id %s from the network", for: .subscription_manager, event.kind, event.id.hex())
|
||||
case .eose:
|
||||
Log.debug("Session subscribe: Received EOSE from the network", for: .subscription_manager)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -207,7 +207,10 @@ class RelayPool {
|
||||
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (RelayURL, NostrConnectionEvent) -> (), to: [RelayURL]? = nil) {
|
||||
Task {
|
||||
await register_handler(sub_id: sub_id, handler: handler)
|
||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
||||
// When the caller specifies no relays, it is implied that the user wants to use the ones in the user relay list. Skip ephemeral relays in that case.
|
||||
// When the caller specifies specific relays, do not skip ephemeral relays to respect the exact list given by the caller.
|
||||
let shouldSkipEphemeralRelays = to == nil ? true : false
|
||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to, skip_ephemeral: shouldSkipEphemeralRelays)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,9 @@ class DamusState: HeadlessDamusState {
|
||||
self.favicon_cache = FaviconCache()
|
||||
|
||||
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
|
||||
self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
|
||||
let nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
|
||||
self.nostrNetwork = nostrNetwork
|
||||
self.wallet.nostrNetwork = nostrNetwork
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -122,7 +124,7 @@ class DamusState: HeadlessDamusState {
|
||||
events: EventCache(ndb: ndb),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
wallet: WalletModel(settings: settings),
|
||||
wallet: WalletModel(settings: settings), // nostrNetwork is connected after initialization
|
||||
nav: navigationCoordinator,
|
||||
music: MusicController(onChange: { _ in }),
|
||||
video: DamusVideoCoordinator(),
|
||||
|
||||
@@ -54,80 +54,6 @@ extension WalletConnect {
|
||||
return ev
|
||||
}
|
||||
|
||||
/// Sends out a wallet balance request to the NWC relay, and ensures that:
|
||||
/// 1. the NWC relay is connected and we are listening to NWC events
|
||||
/// 2. the NWC relay is connected and we are listening to NWC
|
||||
///
|
||||
/// Note: This does not return the actual balance information. The actual balance is handled elsewhere around `HomeModel` and `WalletModel`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The NWC wallet connection URL
|
||||
/// - pool: The relay pool to connect to
|
||||
/// - post: The postbox to send events in
|
||||
/// - delay: The delay before actually sending the request to the network
|
||||
/// - on_flush: A callback to call after the event has been flushed to the network
|
||||
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
||||
@discardableResult
|
||||
static func request_balance_information(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||
let req = WalletConnect.Request.getBalance
|
||||
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
||||
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
||||
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||
return ev
|
||||
}
|
||||
|
||||
/// Sends out a wallet transaction list request to the NWC relay, and ensures that:
|
||||
/// 1. the NWC relay is connected and we are listening to NWC events
|
||||
/// 2. the NWC relay is connected and we are listening to NWC
|
||||
///
|
||||
/// Note: This does not return the actual transaction list. The actual transaction list is handled elsewhere around `HomeModel` and `WalletModel`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The NWC wallet connection URL
|
||||
/// - pool: The relay pool to connect to
|
||||
/// - post: The postbox to send events in
|
||||
/// - delay: The delay before actually sending the request to the network
|
||||
/// - on_flush: A callback to call after the event has been flushed to the network
|
||||
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
||||
@discardableResult
|
||||
static func request_transaction_list(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||
let req = WalletConnect.Request.getTransactionList(from: nil, until: nil, limit: 10, offset: 0, unpaid: false, type: "")
|
||||
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
||||
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
||||
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||
return ev
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func refresh_wallet_information(damus_state: DamusState) async {
|
||||
damus_state.wallet.resetWalletStateInformation()
|
||||
await Self.update_wallet_information(damus_state: damus_state)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func update_wallet_information(damus_state: DamusState) async {
|
||||
guard let url = damus_state.settings.nostr_wallet_connect,
|
||||
let nwc = WalletConnectURL(str: url) else {
|
||||
return
|
||||
}
|
||||
|
||||
let flusher: OnFlush? = nil
|
||||
|
||||
let delay = 0.0 // We don't need a delay when fetching a transaction list or balance
|
||||
|
||||
damus_state.nostrNetwork.requestTransactionList(url: nwc, delay: delay, on_flush: flusher)
|
||||
damus_state.nostrNetwork.requestBalanceInformation(url: nwc, delay: delay, on_flush: flusher)
|
||||
return
|
||||
}
|
||||
|
||||
static func handle_zap_success(state: DamusState, resp: WalletConnect.FullWalletResponse) {
|
||||
// find the pending zap and mark it as pending-confirmed
|
||||
for kv in state.zaps.our_zaps {
|
||||
|
||||
@@ -11,11 +11,24 @@ enum WalletConnectState {
|
||||
case new(WalletConnectURL)
|
||||
case existing(WalletConnectURL)
|
||||
case none
|
||||
|
||||
/// Gets the currently connected NWC URL
|
||||
func currentNwcUrl() -> WalletConnectURL? {
|
||||
switch self {
|
||||
case .new:
|
||||
return nil // User has not confirmed they want to use this yet, so we cannot call it "current"
|
||||
case .existing(let nwcUrl):
|
||||
return nwcUrl
|
||||
case .none:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Models and manages the user's NWC wallet based on the app's settings
|
||||
class WalletModel: ObservableObject {
|
||||
var settings: UserSettingsStore
|
||||
var nostrNetwork: NostrNetworkManager? = nil
|
||||
private(set) var previous_state: WalletConnectState
|
||||
var initial_percent: Int
|
||||
/// The wallet's balance, in sats.
|
||||
@@ -37,6 +50,7 @@ class WalletModel: ObservableObject {
|
||||
self.previous_state = .none
|
||||
self.settings = settings
|
||||
self.initial_percent = settings.donation_percent
|
||||
self.nostrNetwork = nil
|
||||
}
|
||||
|
||||
init(settings: UserSettingsStore) {
|
||||
@@ -50,6 +64,7 @@ class WalletModel: ObservableObject {
|
||||
self.connect_state = .none
|
||||
}
|
||||
self.initial_percent = settings.donation_percent
|
||||
self.nostrNetwork = nil
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
@@ -96,12 +111,114 @@ class WalletModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Wallet internal state lifecycle functions
|
||||
|
||||
@MainActor
|
||||
func resetWalletStateInformation() {
|
||||
self.transactions = nil
|
||||
self.balance = nil
|
||||
}
|
||||
|
||||
|
||||
func refreshWalletInformation() async throws {
|
||||
await self.resetWalletStateInformation()
|
||||
try await loadWalletInformation()
|
||||
}
|
||||
|
||||
func loadWalletInformation() async throws {
|
||||
try await loadBalance()
|
||||
try await loadTransactionList()
|
||||
}
|
||||
|
||||
func loadBalance() async throws {
|
||||
let balance = try await fetchBalance()
|
||||
DispatchQueue.main.async {
|
||||
self.balance = balance
|
||||
}
|
||||
}
|
||||
|
||||
func loadTransactionList() async throws {
|
||||
let transactions = try await fetchTransactions(from: nil, until: nil, limit: 50, offset: 0, unpaid: false, type: "")
|
||||
DispatchQueue.main.async {
|
||||
self.transactions = transactions
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Easy wallet info fetching interface
|
||||
|
||||
func fetchTransactions(from: UInt64?, until: UInt64?, limit: Int?, offset: Int?, unpaid: Bool?, type: String?) async throws -> [WalletConnect.Transaction] {
|
||||
let response = try await self.request(.getTransactionList(from: from, until: until, limit: limit, offset: offset, unpaid: unpaid, type: type))
|
||||
guard case .list_transactions(let transactionResponse) = response else { throw FetchError.responseMismatch }
|
||||
return transactionResponse.transactions
|
||||
}
|
||||
|
||||
|
||||
/// Fetches the balance amount from the network and returns the amount in sats
|
||||
func fetchBalance() async throws -> Int64 {
|
||||
let response = try await self.request(.getBalance)
|
||||
guard case .get_balance(let balanceResponse) = response else { throw FetchError.responseMismatch }
|
||||
return balanceResponse.balance / 1000
|
||||
}
|
||||
|
||||
enum FetchError: Error {
|
||||
case responseMismatch
|
||||
}
|
||||
|
||||
// MARK: - Easy request/response interface
|
||||
|
||||
func request(_ request: WalletConnect.Request, timeout: Duration = .seconds(10)) async throws(WalletRequestError) -> WalletConnect.Response.Result {
|
||||
guard let nostrNetwork else { throw .notConnectedToTheNostrNetwork }
|
||||
guard let currentNwcUrl = self.connect_state.currentNwcUrl() else { throw .noConnectedWallet }
|
||||
guard let requestEvent = request.to_nostr_event(to_pk: currentNwcUrl.pubkey, keypair: currentNwcUrl.keypair) else { throw .errorFormattingRequest }
|
||||
|
||||
let responseFilters = [
|
||||
NostrFilter(
|
||||
kinds: [.nwc_response],
|
||||
referenced_ids: [requestEvent.id],
|
||||
pubkeys: [currentNwcUrl.keypair.pubkey],
|
||||
authors: [currentNwcUrl.pubkey]
|
||||
)
|
||||
]
|
||||
|
||||
nostrNetwork.send(event: requestEvent, to: [currentNwcUrl.relay], skipEphemeralRelays: false)
|
||||
for await item in nostrNetwork.reader.subscribe(filters: responseFilters, to: [currentNwcUrl.relay], timeout: timeout) {
|
||||
switch item {
|
||||
case .event(borrow: let borrow):
|
||||
var responseEvent: NostrEvent? = nil
|
||||
try? borrow { ev in responseEvent = ev.toOwned() }
|
||||
guard let responseEvent else { throw .internalError }
|
||||
|
||||
let fullWalletResponse: WalletConnect.FullWalletResponse
|
||||
do { fullWalletResponse = try WalletConnect.FullWalletResponse(from: responseEvent, nwc: currentNwcUrl) }
|
||||
catch { throw WalletRequestError.walletResponseDecodingError(error) }
|
||||
|
||||
guard fullWalletResponse.req_id == requestEvent.id else { continue } // Our filters may match other responses
|
||||
if let responseError = fullWalletResponse.response.error { throw .walletResponseError(responseError) }
|
||||
|
||||
guard let result = fullWalletResponse.response.result else { throw .walletEmptyResponse }
|
||||
return result
|
||||
case .eose:
|
||||
continue
|
||||
}
|
||||
}
|
||||
do { try Task.checkCancellation() } catch { throw .cancelled }
|
||||
throw .responseTimeout
|
||||
}
|
||||
|
||||
enum WalletRequestError: Error {
|
||||
case notConnectedToTheNostrNetwork
|
||||
case noConnectedWallet
|
||||
case errorFormattingRequest
|
||||
case internalError
|
||||
case walletResponseDecodingError(WalletConnect.FullWalletResponse.InitializationError)
|
||||
case walletResponseMismatch
|
||||
case walletResponseError(WalletConnect.WalletResponseErr)
|
||||
case walletEmptyResponse
|
||||
case responseTimeout
|
||||
case cancelled
|
||||
}
|
||||
|
||||
// MARK: - Async wallet response waiting mechanism
|
||||
|
||||
func waitForResponse(for requestId: NoteId, timeout: Duration = .seconds(10)) async throws -> WalletConnect.Response.Result {
|
||||
|
||||
@@ -45,11 +45,11 @@ struct SendPaymentView: View {
|
||||
break
|
||||
case .completed:
|
||||
// Refresh wallet to reflect new balance after payment
|
||||
Task { await WalletConnect.refresh_wallet_information(damus_state: damus_state) }
|
||||
Task { try await model.refreshWalletInformation() }
|
||||
case .failed:
|
||||
// Even when a wallet says it has failed, update balance just in case it is a false negative,
|
||||
// This might prevent the user from accidentally sending a payment twice in case of a bug.
|
||||
Task { await WalletConnect.refresh_wallet_information(damus_state: damus_state) }
|
||||
Task { try await model.refreshWalletInformation() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ struct WalletView: View {
|
||||
@ObservedObject var model: WalletModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
@State private var showBalance: Bool = false
|
||||
@State private var walletRefreshTask: Task<Void, Never>? = nil
|
||||
|
||||
init(damus_state: DamusState, model: WalletModel? = nil) {
|
||||
self.damus_state = damus_state
|
||||
@@ -104,11 +105,10 @@ struct WalletView: View {
|
||||
}
|
||||
}
|
||||
.onAppear() {
|
||||
Task { await self.updateWalletInformation() }
|
||||
self.refreshWalletInformation()
|
||||
}
|
||||
.refreshable {
|
||||
model.resetWalletStateInformation()
|
||||
await self.updateWalletInformation()
|
||||
self.refreshWalletInformation()
|
||||
}
|
||||
.sheet(isPresented: $show_settings, onDismiss: { self.show_settings = false }) {
|
||||
ScrollView {
|
||||
@@ -127,8 +127,20 @@ struct WalletView: View {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func updateWalletInformation() async {
|
||||
await WalletConnect.update_wallet_information(damus_state: damus_state)
|
||||
func refreshWalletInformation() {
|
||||
walletRefreshTask?.cancel()
|
||||
walletRefreshTask = Task {
|
||||
do {
|
||||
try await self.model.refreshWalletInformation()
|
||||
}
|
||||
catch {
|
||||
guard let error = error as? ErrorView.UserPresentableErrorProtocol else {
|
||||
Log.error("Error while refreshing wallet: %s", for: .nwc, error.localizedDescription)
|
||||
return
|
||||
}
|
||||
present_sheet(.error(error.userPresentableError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,11 @@ struct ErrorView: View {
|
||||
let technical_info: String?
|
||||
|
||||
}
|
||||
|
||||
/// An error that can be displayed to the user, and can be sent to the Developers as well.
|
||||
protocol UserPresentableErrorProtocol: Error {
|
||||
var userPresentableError: UserPresentableError { get }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user