Skip to content

Commit 1024723

Browse files
authored
Merge pull request #28 from lightsparkdev/release/core-v0.3.1
Merge release/core-v0.3.1 into main
2 parents 2342122 + 9399704 commit 1024723

File tree

49 files changed

+1406
-186
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1406
-186
lines changed

androidwalletdemo/src/main/java/com/lightspark/androidwalletdemo/auth/CredentialsStore.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class CredentialsStore(private val context: Context) {
4646
fun getJwtInfoFlow() = context.dataStore.data.map { preferences ->
4747
val accountId = preferences[ACCOUNT_ID_KEY] ?: return@map null
4848
val jwt = preferences[JWT_KEY] ?: return@map null
49-
val userName = preferences[USER_NAME_KEY] ?: return@map null
49+
val userName = preferences[USER_NAME_KEY] ?: ""
5050
SavedCredentials(accountId, jwt, userName)
5151
}.distinctUntilChanged()
5252
}

androidwalletdemo/src/main/java/com/lightspark/androidwalletdemo/sendpayment/SendPaymentViewModel.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import android.util.Log
44
import androidx.lifecycle.ViewModel
55
import androidx.lifecycle.viewModelScope
66
import com.lightspark.androidwalletdemo.util.CurrencyAmountArg
7+
import com.lightspark.androidwalletdemo.util.currencyAmountSats
78
import com.lightspark.androidwalletdemo.util.zeroCurrencyAmount
89
import com.lightspark.androidwalletdemo.util.zeroCurrencyAmountArg
910
import com.lightspark.androidwalletdemo.wallet.PaymentRepository
1011
import com.lightspark.sdk.core.Lce
12+
import com.lightspark.sdk.wallet.model.TransactionStatus
1113
import dagger.hilt.android.lifecycle.HiltViewModel
1214
import javax.inject.Inject
1315
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -42,7 +44,16 @@ class SendPaymentViewModel @Inject constructor(
4244
}
4345
}.filterNotNull().map {
4446
when (it) {
45-
is Lce.Content -> PaymentStatus.SUCCESS
47+
is Lce.Content -> {
48+
when (it.data.status) {
49+
TransactionStatus.PENDING -> PaymentStatus.PENDING
50+
TransactionStatus.SUCCESS -> PaymentStatus.SUCCESS
51+
else -> {
52+
Log.e("SendPaymentViewModel", "Error sending payment")
53+
PaymentStatus.FAILURE
54+
}
55+
}
56+
}
4657
is Lce.Error -> {
4758
Log.e("SendPaymentViewModel", "Error sending payment", it.exception)
4859
PaymentStatus.FAILURE
@@ -62,7 +73,9 @@ class SendPaymentViewModel @Inject constructor(
6273
.onEach {
6374
(it as? Lce.Content)?.let { invoiceData ->
6475
invoiceData.data?.amount?.let { decodedInvoiceAmount ->
65-
paymentAmountFlow.tryEmit(decodedInvoiceAmount)
76+
// Default to paying 10 sats for 0 amount invoices.
77+
val amount = if (decodedInvoiceAmount.originalValue > 0) decodedInvoiceAmount else currencyAmountSats(10)
78+
paymentAmountFlow.tryEmit(amount)
6679
}
6780
}
6881
}

androidwalletdemo/src/main/java/com/lightspark/androidwalletdemo/wallet/PaymentRepository.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.lightspark.androidwalletdemo.wallet
22

33
import com.lightspark.androidwalletdemo.util.CurrencyAmountArg
4+
import com.lightspark.sdk.core.asLce
45
import com.lightspark.sdk.core.wrapWithLceFlow
56
import com.lightspark.sdk.wallet.LightsparkCoroutinesWalletClient
67
import com.lightspark.sdk.wallet.model.InvoiceType
@@ -16,8 +17,8 @@ class PaymentRepository @Inject constructor(private val lightsparkClient: Lights
1617
lightsparkClient.createInvoice(amountMillis, memo, type)
1718
}.flowOn(Dispatchers.IO)
1819

19-
fun payInvoice(invoice: String) =
20-
wrapWithLceFlow { lightsparkClient.payInvoice(invoice, 1000000) }.flowOn(Dispatchers.IO)
20+
suspend fun payInvoice(invoice: String) =
21+
lightsparkClient.payInvoiceAndAwaitCompletion(invoice, 1000000).asLce().flowOn(Dispatchers.IO)
2122

2223
fun decodeInvoice(encodedInvoice: String) =
2324
wrapWithLceFlow { lightsparkClient.decodeInvoice(encodedInvoice) }.flowOn(Dispatchers.IO)

androidwalletdemo/src/main/java/com/lightspark/androidwalletdemo/wallet/WalletRepository.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.lightspark.sdk.core.Lce
66
import com.lightspark.sdk.core.asLce
77
import com.lightspark.sdk.core.auth.AuthProvider
88
import com.lightspark.sdk.core.crypto.androidKeystoreContainsPrivateKeyForAlias
9+
import com.lightspark.sdk.core.crypto.generateSigningKeyPair
910
import com.lightspark.sdk.core.crypto.generateSigningKeyPairInAndroidKeyStore
1011
import com.lightspark.sdk.core.requester.ServerEnvironment
1112
import com.lightspark.sdk.core.wrapWithLceFlow
@@ -51,8 +52,8 @@ class WalletRepository @Inject constructor(
5152
// able to export that key for the user, but it does come with increased security. If you'd like to manage your
5253
// own keys or store them in some other way in your own app code, you can still generate a valid key pair using
5354
// the [generateSigningKeyPair] function in the SDK.
54-
val keyPair = generateSigningKeyPairInAndroidKeyStore(LIGHTSPARK_SIGNING_KEY_ALIAS)
55-
walletClient.loadWalletSigningKeyAlias(LIGHTSPARK_SIGNING_KEY_ALIAS)
55+
val keyPair = generateSigningKeyPair()
56+
walletClient.loadWalletSigningKey(keyPair.private.encoded)
5657
return walletClient.initializeWalletAndWaitForInitialized(
5758
keyType = KeyType.RSA_OAEP,
5859
signingPublicKey = Base64.encodeToString(keyPair.public.encoded, Base64.NO_WRAP),

core/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
GROUP=com.lightspark
22
POM_ARTIFACT_ID=lightspark-core
33
# Don't bump this manually. Run `scripts/versions.main.kt <new_version>` to bump the version instead.
4-
VERSION_NAME=0.3.0
4+
VERSION_NAME=0.3.1
55

66
POM_DESCRIPTION=The Lightspark shared utilities library for Kotlin and Java.
77
POM_INCEPTION_YEAR=2023
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.lightspark.sdk.core.requester
2+
3+
import com.lightspark.sdk.core.LightsparkErrorCode
4+
import com.lightspark.sdk.core.LightsparkException
5+
import io.ktor.client.plugins.websocket.ClientWebSocketSession
6+
import io.ktor.websocket.close
7+
import io.ktor.websocket.send
8+
import kotlinx.coroutines.flow.catch
9+
import kotlinx.coroutines.flow.map
10+
import kotlinx.coroutines.flow.receiveAsFlow
11+
import kotlinx.coroutines.withTimeout
12+
import kotlinx.serialization.decodeFromString
13+
import kotlinx.serialization.json.Json
14+
import kotlinx.serialization.json.JsonObject
15+
import kotlinx.serialization.json.jsonObject
16+
import kotlinx.serialization.json.jsonPrimitive
17+
18+
const val CONNECTION_INIT_TIMEOUT = 10_000L
19+
20+
/**
21+
* An implementation of https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md for use with a Ktor client.
22+
* It can carry queries in addition to subscriptions over the websocket
23+
*/
24+
internal class GraphQLWebsocketProtocol(
25+
private val webSocketSession: ClientWebSocketSession,
26+
private val listener: GraphQLWebsocketListener,
27+
private val jsonSerialFormat: Json,
28+
private val connectionPayload: suspend () -> JsonObject? = { null },
29+
) {
30+
suspend fun connectionInit() {
31+
val payload = connectionPayload()
32+
33+
sendMessage {
34+
add("type", "connection_init")
35+
payload?.let { add("payload", it) }
36+
}
37+
waitForConnectionAck()
38+
}
39+
40+
suspend fun run() {
41+
// TODO(Jeremy): Consider adding client-side ping.
42+
webSocketSession.incoming
43+
.receiveAsFlow()
44+
.catch { cause ->
45+
listener.onError(cause)
46+
}
47+
.map {
48+
jsonSerialFormat.decodeFromString(JsonObject.serializer(), it.data.decodeToString())
49+
}.collect {
50+
handleServerMessage(it)
51+
}
52+
}
53+
54+
suspend fun <T> sendQuery(id: String, query: Query<T>) {
55+
val operationNameRegex =
56+
Regex("^\\s*(query|mutation|subscription)\\s+(\\w+)", RegexOption.IGNORE_CASE)
57+
val operationMatch = operationNameRegex.find(query.queryPayload)
58+
if (operationMatch == null || operationMatch.groupValues.size < 3) {
59+
throw LightsparkException("Invalid query payload", LightsparkErrorCode.INVALID_QUERY)
60+
}
61+
val operation = operationMatch.groupValues[2]
62+
// TODO(Jeremy): Handle the signing node ID.
63+
sendMessage {
64+
add("id", id)
65+
add("type", "subscribe")
66+
val payload = buildJsonObject(jsonSerialFormat) {
67+
add("query", query.queryPayload)
68+
add("variables", buildJsonObject(jsonSerialFormat, query.variableBuilder))
69+
add("operationName", operation)
70+
}
71+
add("payload", payload)
72+
}
73+
}
74+
75+
suspend fun stopQuery(id: String) {
76+
sendMessage {
77+
add("id", id)
78+
add("type", "complete")
79+
}
80+
}
81+
82+
suspend fun close() {
83+
webSocketSession.close()
84+
}
85+
86+
private suspend fun waitForConnectionAck() {
87+
withTimeout(CONNECTION_INIT_TIMEOUT) {
88+
while (true) {
89+
val received = webSocketSession.incoming.receive()
90+
try {
91+
val receivedText = received.data.decodeToString()
92+
val receivedJson = jsonSerialFormat.decodeFromString<JsonObject>(
93+
receivedText,
94+
)
95+
when (val type = receivedJson["type"]?.jsonPrimitive?.content) {
96+
"connection_ack" -> return@withTimeout
97+
"ping" -> sendPong()
98+
else -> {
99+
listener.onError(Exception("Unexpected message type: $type"))
100+
return@withTimeout
101+
}
102+
}
103+
} catch (e: Exception) {
104+
listener.onError(e)
105+
return@withTimeout
106+
}
107+
}
108+
}
109+
}
110+
111+
private suspend fun handleServerMessage(messageJson: JsonObject) {
112+
when (messageJson["type"]?.jsonPrimitive?.content) {
113+
"next" -> {
114+
val payload = messageJson["payload"]?.jsonObject
115+
val id = messageJson["id"]?.jsonPrimitive?.content
116+
if (id != null && payload != null) {
117+
listener.onOperationMessage(id, payload)
118+
}
119+
}
120+
121+
"error" -> {
122+
val payload = messageJson["payload"]?.jsonObject
123+
val id = messageJson["id"]?.jsonPrimitive?.content
124+
val errors = payload?.get("errors")?.jsonObject
125+
if (id != null && errors != null) {
126+
listener.onOperationError(id, errors)
127+
}
128+
}
129+
130+
"complete" -> {
131+
messageJson["id"]?.jsonPrimitive?.content?.let {
132+
listener.operationComplete(it)
133+
}
134+
}
135+
136+
"ping" -> sendPong()
137+
"pong" -> Unit // Nothing to do, the server acknowledged one of our pings
138+
else -> Unit // Unknown message
139+
}
140+
}
141+
142+
private suspend fun sendPong() {
143+
sendMessage {
144+
add("type", "pong")
145+
}
146+
}
147+
148+
private suspend fun sendMessage(payloadBuilder: JsonObjectBuilder.() -> Unit) {
149+
val jsonObjectBuilder = buildJsonObject(jsonSerialFormat, payloadBuilder)
150+
webSocketSession.send(jsonObjectBuilder.toString())
151+
}
152+
}
153+
154+
internal interface GraphQLWebsocketListener {
155+
fun onOperationMessage(id: String, payload: JsonObject)
156+
fun onOperationError(id: String, payload: JsonObject)
157+
fun operationComplete(id: String)
158+
fun onError(error: Throwable)
159+
fun onClose(code: Int, reason: String)
160+
}
161+
162+
@Suppress("unused")
163+
enum class CloseCode(val code: Int) {
164+
NormalClosure(1000),
165+
GoingAway(1001),
166+
AbnormalClosure(1006),
167+
NoStatusReceived(1005),
168+
ServiceRestart(1012),
169+
TryAgainLater(1013),
170+
BadGateway(1013),
171+
172+
InternalServerError(4500),
173+
InternalClientError(4005),
174+
BadRequest(4400),
175+
BadResponse(4004),
176+
Unauthorized(4401),
177+
Forbidden(4403),
178+
SubprotocolNotAcceptable(4406),
179+
ConnectionInitialisationTimeout(4408),
180+
ConnectionAcknowledgementTimeout(4504),
181+
SubscriberAlreadyExists(4409),
182+
TooManyInitialisationRequests(4429),
183+
184+
Terminated(4499),
185+
}

core/src/commonMain/kotlin/com/lightspark/sdk/core/requester/VariableBuilder.kt renamed to core/src/commonMain/kotlin/com/lightspark/sdk/core/requester/JsonObjectBuilder.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import kotlinx.serialization.json.JsonObject
66
import kotlinx.serialization.json.JsonPrimitive
77
import kotlinx.serialization.json.encodeToJsonElement
88

9-
class VariableBuilder(val jsonSerialFormat: Json) {
9+
class JsonObjectBuilder(val jsonSerialFormat: Json) {
1010
val variables = mutableMapOf<String, JsonElement>()
1111

1212
fun add(name: String, value: JsonElement) {
@@ -38,8 +38,16 @@ class VariableBuilder(val jsonSerialFormat: Json) {
3838
}
3939
}
4040

41-
fun variables(jsonSerialFormat: Json, builder: VariableBuilder.() -> Unit): JsonObject {
42-
val variableBuilder = VariableBuilder(jsonSerialFormat)
43-
variableBuilder.builder()
44-
return variableBuilder.build()
41+
fun buildJsonObject(jsonSerialFormat: Json, builder: JsonObjectBuilder.() -> Unit): JsonObject {
42+
val jsonObjectBuilder = JsonObjectBuilder(jsonSerialFormat)
43+
jsonObjectBuilder.builder()
44+
return jsonObjectBuilder.build()
45+
}
46+
47+
fun Map<String, Any?>.toJsonObject(jsonSerialFormat: Json): JsonObject {
48+
val jsonObjectBuilder = JsonObjectBuilder(jsonSerialFormat)
49+
forEach { (key, value) ->
50+
jsonObjectBuilder.add(key, value)
51+
}
52+
return jsonObjectBuilder.build()
4553
}

core/src/commonMain/kotlin/com/lightspark/sdk/core/requester/Query.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.lightspark.sdk.core.requester
22

3+
import java.util.UUID
34
import kotlin.jvm.JvmOverloads
45
import kotlinx.serialization.json.JsonObject
56

@@ -9,10 +10,12 @@ interface StringDeserializer<T> {
910

1011
data class Query<T>(
1112
val queryPayload: String,
12-
val variableBuilder: VariableBuilder.() -> Unit,
13+
val variableBuilder: JsonObjectBuilder.() -> Unit,
1314
val signingNodeId: String? = null,
1415
val deserializer: (JsonObject) -> T,
1516
) {
17+
val id = UUID.randomUUID().toString()
18+
1619
/**
1720
* This constructor is for convenience when calling from Java rather than Kotlin. The primary constructor is
1821
* simpler to use from Kotlin if possible.

0 commit comments

Comments
 (0)