Adjusts the Wallet Connect API to the new RPC design
This commit is contained in:
@@ -50,6 +50,7 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="nostrwalletconnect" />
|
<data android:scheme="nostrwalletconnect" />
|
||||||
|
<data android:scheme="nostr+walletconnect" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import android.content.res.Resources
|
|||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.FileHeader
|
import com.vitorpamplona.amethyst.service.FileHeader
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.Constants
|
import com.vitorpamplona.amethyst.service.relays.Constants
|
||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
@@ -165,13 +168,19 @@ class Account(
|
|||||||
return zapPaymentRequest != null
|
return zapPaymentRequest != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendZapPaymentRequestFor(lnInvoice: String) {
|
fun sendZapPaymentRequestFor(bolt11: String, onResponse: (LnZapPaymentResponseEvent) -> Unit) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
zapPaymentRequest?.let {
|
zapPaymentRequest?.let {
|
||||||
val event = LnZapPaymentRequestEvent.create(lnInvoice, it.pubKeyHex, it.secret?.toByteArray() ?: loggedIn.privKey!!)
|
val event = LnZapPaymentRequestEvent.create(bolt11, it.pubKeyHex, it.secret?.toByteArray() ?: loggedIn.privKey!!)
|
||||||
|
|
||||||
Client.send(event, it.relayUri)
|
val wcListener = NostrLnZapPaymentResponseDataSource(it.pubKeyHex, loggedIn.pubKey.toHexKey(), event.id)
|
||||||
|
wcListener.start()
|
||||||
|
|
||||||
|
LocalCache.consume(event, onResponse)
|
||||||
|
Client.send(event, it.relayUri, wcListener.feedTypes) {
|
||||||
|
wcListener.destroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ object LocalCache {
|
|||||||
val channels = ConcurrentHashMap<HexKey, Channel>()
|
val channels = ConcurrentHashMap<HexKey, Channel>()
|
||||||
val addressables = ConcurrentHashMap<String, AddressableNote>(100)
|
val addressables = ConcurrentHashMap<String, AddressableNote>(100)
|
||||||
|
|
||||||
|
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, (LnZapPaymentResponseEvent) -> Unit>(10)
|
||||||
|
|
||||||
fun checkGetOrCreateUser(key: String): User? {
|
fun checkGetOrCreateUser(key: String): User? {
|
||||||
if (isValidHexNpub(key)) {
|
if (isValidHexNpub(key)) {
|
||||||
return getOrCreateUser(key)
|
return getOrCreateUser(key)
|
||||||
@@ -675,6 +677,22 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(event: LnZapPaymentRequestEvent) {
|
||||||
|
// Does nothing without a response callback.
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(event: LnZapPaymentRequestEvent, onResponse: (LnZapPaymentResponseEvent) -> Unit) {
|
||||||
|
awaitingPaymentRequests.put(event.id, onResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consume(event: LnZapPaymentResponseEvent) {
|
||||||
|
val responseCallback = awaitingPaymentRequests[event.requestId()]
|
||||||
|
|
||||||
|
if (responseCallback != null) {
|
||||||
|
responseCallback(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun findUsersStartingWith(username: String): List<User> {
|
fun findUsersStartingWith(username: String): List<User> {
|
||||||
return users.values.filter {
|
return users.values.filter {
|
||||||
(it.anyNameStartsWith(username)) ||
|
(it.anyNameStartsWith(username)) ||
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ object Nip47 {
|
|||||||
|
|
||||||
val url = Uri.parse(uri)
|
val url = Uri.parse(uri)
|
||||||
|
|
||||||
if (url.scheme != "nostrwalletconnect") {
|
if (url.scheme != "nostrwalletconnect" || url.scheme != "nostr+walletconnect") {
|
||||||
throw IllegalArgumentException("Not a Wallet Connect QR Code")
|
throw IllegalArgumentException("Not a Wallet Connect QR Code")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
|||||||
RepostEvent.kind,
|
RepostEvent.kind,
|
||||||
ReportEvent.kind,
|
ReportEvent.kind,
|
||||||
LnZapEvent.kind,
|
LnZapEvent.kind,
|
||||||
|
LnZapPaymentResponseEvent.kind,
|
||||||
ChannelMessageEvent.kind,
|
ChannelMessageEvent.kind,
|
||||||
BadgeAwardEvent.kind
|
BadgeAwardEvent.kind
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import kotlin.Error
|
||||||
|
|
||||||
abstract class NostrDataSource(val debugName: String) {
|
abstract class NostrDataSource(val debugName: String) {
|
||||||
private var subscriptions = mapOf<String, Subscription>()
|
private var subscriptions = mapOf<String, Subscription>()
|
||||||
@@ -81,6 +82,8 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
LocalCache.consume(event)
|
LocalCache.consume(event)
|
||||||
}
|
}
|
||||||
is LnZapRequestEvent -> LocalCache.consume(event)
|
is LnZapRequestEvent -> LocalCache.consume(event)
|
||||||
|
is LnZapPaymentRequestEvent -> LocalCache.consume(event)
|
||||||
|
is LnZapPaymentResponseEvent -> LocalCache.consume(event)
|
||||||
is LongTextNoteEvent -> LocalCache.consume(event, relay)
|
is LongTextNoteEvent -> LocalCache.consume(event, relay)
|
||||||
is MetadataEvent -> LocalCache.consume(event)
|
is MetadataEvent -> LocalCache.consume(event)
|
||||||
is PrivateDmEvent -> LocalCache.consume(event, relay)
|
is PrivateDmEvent -> LocalCache.consume(event, relay)
|
||||||
@@ -129,6 +132,11 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
Client.subscribe(clientListener)
|
Client.subscribe(clientListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
stop()
|
||||||
|
Client.unsubscribe(clientListener)
|
||||||
|
}
|
||||||
|
|
||||||
open fun start() {
|
open fun start() {
|
||||||
println("DataSource: ${this.javaClass.simpleName} Start")
|
println("DataSource: ${this.javaClass.simpleName} Start")
|
||||||
resetFilters()
|
resetFilters()
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LnZapPaymentResponseEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
|
|
||||||
|
class NostrLnZapPaymentResponseDataSource(
|
||||||
|
private var fromServiceHex: String,
|
||||||
|
private var toUserHex: String,
|
||||||
|
private var replyingToHex: String,
|
||||||
|
): NostrDataSource("LnZapPaymentResponseFeed") {
|
||||||
|
|
||||||
|
val feedTypes = setOf(FeedType.WALLET_CONNECT)
|
||||||
|
|
||||||
|
private fun createWalletConnectServiceWatcher(): TypedFilter {
|
||||||
|
// downloads all the reactions to a given event.
|
||||||
|
return TypedFilter(
|
||||||
|
types = feedTypes,
|
||||||
|
filter = JsonFilter(
|
||||||
|
kinds = listOf(LnZapPaymentResponseEvent.kind),
|
||||||
|
authors = listOf(fromServiceHex),
|
||||||
|
tags = mapOf(
|
||||||
|
"e" to listOf(replyingToHex),
|
||||||
|
"p" to listOf(toUserHex)
|
||||||
|
),
|
||||||
|
limit = 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val channel = requestNewChannel()
|
||||||
|
|
||||||
|
override fun updateChannelFilters() {
|
||||||
|
val wc = createWalletConnectServiceWatcher()
|
||||||
|
|
||||||
|
channel.typedFilters = listOfNotNull(wc).ifEmpty { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -225,6 +225,7 @@ open class Event(
|
|||||||
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
FileHeaderEvent.kind -> FileHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
+23
-1
@@ -1,6 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.service.model
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.vitorpamplona.amethyst.model.HexKey
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
@@ -16,6 +17,8 @@ class LnZapPaymentRequestEvent(
|
|||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
fun walletServicePubKey() = tags.firstOrNull() { it.size > 1 && it[0] == "p" }?.get(1)
|
||||||
|
|
||||||
fun lnInvoice(privKey: ByteArray): String? {
|
fun lnInvoice(privKey: ByteArray): String? {
|
||||||
return try {
|
return try {
|
||||||
val sharedSecret = Utils.getSharedSecret(privKey, pubKey.toByteArray())
|
val sharedSecret = Utils.getSharedSecret(privKey, pubKey.toByteArray())
|
||||||
@@ -37,9 +40,10 @@ class LnZapPaymentRequestEvent(
|
|||||||
createdAt: Long = Date().time / 1000
|
createdAt: Long = Date().time / 1000
|
||||||
): LnZapPaymentRequestEvent {
|
): LnZapPaymentRequestEvent {
|
||||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||||
|
val serializedRequest = gson.toJson(PayInvoiceMethod(lnInvoice))
|
||||||
|
|
||||||
val content = Utils.encrypt(
|
val content = Utils.encrypt(
|
||||||
lnInvoice,
|
serializedRequest,
|
||||||
privateKey,
|
privateKey,
|
||||||
walletServicePubkey.toByteArray()
|
walletServicePubkey.toByteArray()
|
||||||
)
|
)
|
||||||
@@ -53,3 +57,21 @@ class LnZapPaymentRequestEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REQUEST OBJECTS
|
||||||
|
|
||||||
|
abstract class Request(val method: String, val params: Params)
|
||||||
|
abstract class Params
|
||||||
|
|
||||||
|
|
||||||
|
// PayInvoice Call
|
||||||
|
|
||||||
|
class PayInvoiceMethod(bolt11: String): Request("pay_invoice", PayInvoiceParams(bolt11)) {
|
||||||
|
class PayInvoiceParams(val invoice: String): Params()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.model
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import com.vitorpamplona.amethyst.model.HexKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
|
import nostr.postr.Utils
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class LnZapPaymentResponseEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
fun requestAuthor() = tags.firstOrNull() { it.size > 1 && it[0] == "p" }?.get(1)
|
||||||
|
fun requestId() = tags.firstOrNull() { it.size > 1 && it[0] == "e" }?.get(1)
|
||||||
|
|
||||||
|
fun response(): Response? = try {
|
||||||
|
if (content.isNotEmpty()) {
|
||||||
|
gson.fromJson(content, Response::class.java)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("LnZapPaymentResponseEvent", "Can't parse content as a payment response: $content", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 23195
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RESPONSE OBJECTS
|
||||||
|
abstract class Response(
|
||||||
|
@SerializedName("result_type")
|
||||||
|
val resultType: String
|
||||||
|
)
|
||||||
|
|
||||||
|
// PayInvoice Call
|
||||||
|
|
||||||
|
class PayInvoiceSuccessResponse(val result: PayInvoiceResultParams):
|
||||||
|
Response("pay_invoice")
|
||||||
|
{
|
||||||
|
class PayInvoiceResultParams(val preimage: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PayInvoiceErrorResponse(val error: PayInvoiceErrorParams? = null):
|
||||||
|
Response("pay_invoice")
|
||||||
|
{
|
||||||
|
class PayInvoiceErrorParams(val code: ErrorType?, val message: String?)
|
||||||
|
|
||||||
|
enum class ErrorType {
|
||||||
|
@SerializedName(value = "rate_limited", alternate = ["RATE_LIMITED"])
|
||||||
|
RATE_LIMITED, // The client is sending commands too fast. It should retry in a few seconds.
|
||||||
|
@SerializedName(value = "not_implemented", alternate = ["NOT_IMPLEMENTED"])
|
||||||
|
NOT_IMPLEMENTED, // The command is not known or is intentionally not implemented.
|
||||||
|
@SerializedName(value = "insufficient_balance", alternate = ["INSUFFICIENT_BALANCE"])
|
||||||
|
INSUFFICIENT_BALANCE, // The wallet does not have enough funds to cover a fee reserve or the payment amount.
|
||||||
|
@SerializedName(value = "quota_exceeded", alternate = ["QUOTA_EXCEEDED"])
|
||||||
|
QUOTA_EXCEEDED, // The wallet has exceeded its spending quota.
|
||||||
|
@SerializedName(value = "restricted", alternate = ["RESTRICTED"])
|
||||||
|
RESTRICTED, // This public key is not allowed to do this operation.
|
||||||
|
@SerializedName(value = "unauthorized", alternate = ["UNAUTHORIZED"])
|
||||||
|
UNAUTHORIZED, // This public key has no wallet connected.
|
||||||
|
@SerializedName(value = "internal", alternate = ["INTERNAL"])
|
||||||
|
INTERNAL, // An internal error.
|
||||||
|
@SerializedName(value = "other", alternate = ["OTHER"])
|
||||||
|
OTHER, // Other error.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package com.vitorpamplona.amethyst.service.relays
|
package com.vitorpamplona.amethyst.service.relays
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrDataSource
|
||||||
import com.vitorpamplona.amethyst.service.model.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
import com.vitorpamplona.amethyst.service.model.EventInterface
|
import com.vitorpamplona.amethyst.service.model.EventInterface
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,22 +67,43 @@ object Client : RelayPool.Listener {
|
|||||||
RelayPool.sendFilterOnlyIfDisconnected()
|
RelayPool.sendFilterOnlyIfDisconnected()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun send(signedEvent: EventInterface, relay: String? = null) {
|
fun send(signedEvent: EventInterface, relay: String? = null, feedTypes: Set<FeedType>? = null, onDone: (() -> Unit)? = null) {
|
||||||
if (relay == null) {
|
if (relay == null) {
|
||||||
RelayPool.send(signedEvent)
|
RelayPool.send(signedEvent)
|
||||||
} else {
|
} else {
|
||||||
val useConnectedRelay = relays.filter { it.url == relay }
|
val useConnectedRelayIfPresent = relays.filter { it.url == relay }
|
||||||
|
|
||||||
if (useConnectedRelay.isNotEmpty()) {
|
if (useConnectedRelayIfPresent.isNotEmpty()) {
|
||||||
useConnectedRelay.forEach {
|
useConnectedRelayIfPresent.forEach {
|
||||||
it.send(signedEvent)
|
it.send(signedEvent)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/** temporary connection */
|
/** temporary connection */
|
||||||
Relay(relay, false, true, emptySet()).requestAndWatch() {
|
newSporadicRelay(relay, feedTypes,
|
||||||
it.send(signedEvent)
|
onConnected = { relay ->
|
||||||
it.disconnect()
|
relay.send(signedEvent)
|
||||||
}
|
},
|
||||||
|
onDone = onDone
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
fun newSporadicRelay(url: String, feedTypes: Set<FeedType>?, onConnected: (Relay) -> Unit, onDone: (() -> Unit)?) {
|
||||||
|
val relay = Relay(url, true, true, feedTypes ?: emptySet())
|
||||||
|
RelayPool.addRelay(relay)
|
||||||
|
|
||||||
|
relay.requestAndWatch {
|
||||||
|
onConnected(relay)
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
delay(10000) // waits for a reply
|
||||||
|
relay.disconnect()
|
||||||
|
RelayPool.removeRelay(relay)
|
||||||
|
|
||||||
|
if (onDone != null)
|
||||||
|
onDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package com.vitorpamplona.amethyst.service.relays
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
import com.vitorpamplona.amethyst.service.model.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
import com.vitorpamplona.amethyst.service.model.EventInterface
|
import com.vitorpamplona.amethyst.service.model.EventInterface
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LnZapPaymentResponseEvent
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -13,7 +15,7 @@ import okhttp3.WebSocketListener
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
enum class FeedType {
|
enum class FeedType {
|
||||||
FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL, SEARCH
|
FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL, SEARCH, WALLET_CONNECT
|
||||||
}
|
}
|
||||||
|
|
||||||
class Relay(
|
class Relay(
|
||||||
@@ -93,11 +95,18 @@ class Relay(
|
|||||||
val type = msg[0].asString
|
val type = msg[0].asString
|
||||||
val channel = msg[1].asString
|
val channel = msg[1].asString
|
||||||
|
|
||||||
|
Log.w("Relay", "New Message $type, $url, $channel, ${msg[2]}")
|
||||||
|
|
||||||
when (type) {
|
when (type) {
|
||||||
"EVENT" -> {
|
"EVENT" -> {
|
||||||
|
val event = Event.fromJson(msg[2], Client.lenient)
|
||||||
|
if (event.kind == LnZapPaymentResponseEvent.kind) {
|
||||||
|
println("This " + event.toJson())
|
||||||
|
}
|
||||||
|
|
||||||
// Log.w("Relay", "Relay onEVENT $url, $channel")
|
// Log.w("Relay", "Relay onEVENT $url, $channel")
|
||||||
listeners.forEach {
|
listeners.forEach {
|
||||||
it.onEvent(this@Relay, channel, Event.fromJson(msg[2], Client.lenient))
|
it.onEvent(this@Relay, channel, event)
|
||||||
if (afterEOSE) {
|
if (afterEOSE) {
|
||||||
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
|
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
|
||||||
}
|
}
|
||||||
@@ -109,15 +118,15 @@ class Relay(
|
|||||||
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
|
it.onRelayStateChange(this@Relay, Type.EOSE, channel)
|
||||||
}
|
}
|
||||||
"NOTICE" -> listeners.forEach {
|
"NOTICE" -> listeners.forEach {
|
||||||
// Log.w("Relay", "Relay onNotice $url, $channel")
|
Log.w("Relay", "Relay onNotice $url, $channel")
|
||||||
it.onError(this@Relay, channel, Error("Relay sent notice: " + channel))
|
it.onError(this@Relay, channel, Error("Relay sent notice: " + channel))
|
||||||
}
|
}
|
||||||
"OK" -> listeners.forEach {
|
"OK" -> listeners.forEach {
|
||||||
// Log.w("Relay", "Relay onOK $url, $channel")
|
Log.w("Relay", "Relay on OK $url, $channel")
|
||||||
it.onSendResponse(this@Relay, msg[1].asString, msg[2].asBoolean, msg[3].asString)
|
it.onSendResponse(this@Relay, msg[1].asString, msg[2].asBoolean, msg[3].asString)
|
||||||
}
|
}
|
||||||
else -> listeners.forEach {
|
else -> listeners.forEach {
|
||||||
// Log.w("Relay", "Relay something else $url, $channel")
|
Log.w("Relay", "Relay something else $url, $channel")
|
||||||
it.onError(
|
it.onError(
|
||||||
this@Relay,
|
this@Relay,
|
||||||
channel,
|
channel,
|
||||||
@@ -213,6 +222,26 @@ class Relay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendUnregisteredFilter(request: JsonFilter, subscriptionId: String?) {
|
||||||
|
if (read) {
|
||||||
|
if (isConnected()) {
|
||||||
|
if (isReady) {
|
||||||
|
val request = """["REQ","${subscriptionId ?: ""}",${request.toJson(url)}]"""
|
||||||
|
println("FILTERSSENT $url $request")
|
||||||
|
socket?.send(request)
|
||||||
|
eventUploadCounterInBytes += request.bytesUsedInMemory()
|
||||||
|
afterEOSE = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// waits 60 seconds to reconnect after disconnected.
|
||||||
|
if (Date().time / 1000 > closingTime + 60) {
|
||||||
|
// sends all filters after connection is successful.
|
||||||
|
requestAndWatch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun sendFilterOnlyIfDisconnected() {
|
fun sendFilterOnlyIfDisconnected() {
|
||||||
if (socket == null) {
|
if (socket == null) {
|
||||||
// waits 60 seconds to reconnect after disconnected.
|
// waits 60 seconds to reconnect after disconnected.
|
||||||
|
|||||||
+17
-1
@@ -15,6 +15,7 @@ import com.vitorpamplona.amethyst.model.Note
|
|||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PayInvoiceErrorResponse
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -80,7 +81,22 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
|||||||
onSuccess = {
|
onSuccess = {
|
||||||
onProgress(0.7f)
|
onProgress(0.7f)
|
||||||
if (account.hasWalletConnectSetup()) {
|
if (account.hasWalletConnectSetup()) {
|
||||||
account.sendZapPaymentRequestFor(it)
|
account.sendZapPaymentRequestFor(
|
||||||
|
bolt11 = it,
|
||||||
|
onResponse = {
|
||||||
|
val response = it.response()
|
||||||
|
if (response is PayInvoiceErrorResponse) {
|
||||||
|
onProgress(0.0f)
|
||||||
|
onError(
|
||||||
|
response.error?.message
|
||||||
|
?: response.error?.code?.toString()
|
||||||
|
?: "Error parsing error message"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// awaits for confirmation from Receiver or timeout.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
onProgress(0.8f)
|
onProgress(0.8f)
|
||||||
|
|
||||||
// Awaits for the event to come back to LocalCache.
|
// Awaits for the event to come back to LocalCache.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.gestures.scrollBy
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -59,6 +60,9 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
|
|||||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.IdentityClaim
|
import com.vitorpamplona.amethyst.service.model.IdentityClaim
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LnZapPaymentResponseEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PayInvoiceErrorResponse
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PayInvoiceSuccessResponse
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
|
||||||
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
|
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
|
||||||
@@ -420,6 +424,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
|
|||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.Bottom) {
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
user.bestDisplayName()?.let {
|
user.bestDisplayName()?.let {
|
||||||
@@ -561,7 +566,28 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
|
|||||||
onSuccess = {
|
onSuccess = {
|
||||||
// pay directly
|
// pay directly
|
||||||
if (account.hasWalletConnectSetup()) {
|
if (account.hasWalletConnectSetup()) {
|
||||||
account.sendZapPaymentRequestFor(it)
|
account.sendZapPaymentRequestFor(it) {
|
||||||
|
val response = it.response()
|
||||||
|
if (response is PayInvoiceSuccessResponse) {
|
||||||
|
scope.launch {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Payment Successful", // Turn this into a UI animation
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
} else if (response is PayInvoiceErrorResponse) {
|
||||||
|
scope.launch {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
response.error?.message
|
||||||
|
?: response.error?.code?.toString()
|
||||||
|
?: "Error parsing error message",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
runCatching {
|
runCatching {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$it"))
|
||||||
|
|||||||
Reference in New Issue
Block a user