Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mdavis/kt uma invoice #54

Merged
merged 11 commits into from
Aug 22, 2024
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ ktlint_function_signature_body_expression_wrapping = default
ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = unset
ktlint_ignore_back_ticked_identifier = false
ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_annotation = disabled
max_line_length = 120
26 changes: 14 additions & 12 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import com.vanniktech.maven.publish.SonatypeHost
import java.net.URL
import org.jetbrains.dokka.gradle.DokkaMultiModuleTask
import org.jetbrains.dokka.gradle.DokkaTaskPartial
import java.net.URL

buildscript {
dependencies {
Expand All @@ -28,8 +28,9 @@ subprojects {
apply(plugin = "org.jlleitschuh.gradle.ktlint")
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
outputToConsole.set(true)
debug.set(true)
verbose.set(true)
disabledRules.set(listOf("no-wildcard-imports"))
disabledRules.set(setOf("no-wildcard-imports"))
}

tasks.create<Exec>("bumpAndTagVersion") {
Expand Down Expand Up @@ -109,16 +110,17 @@ tasks.named<DokkaMultiModuleTask>("dokkaHtmlMultiModule") {
moduleName.set("UMA Kotlin+Java SDKs")
pluginsMapConfiguration.set(
mapOf(
"org.jetbrains.dokka.base.DokkaBase" to """
{
"customStyleSheets": [
"${rootDir.resolve("docs/css/logo-styles.css")}"
],
"customAssets" : [
"${rootDir.resolve("docs/images/uma-logo-white.svg")}"
]
}
""".trimIndent(),
"org.jetbrains.dokka.base.DokkaBase" to
"""
{
"customStyleSheets": [
"${rootDir.resolve("docs/css/logo-styles.css")}"
],
"customAssets" : [
"${rootDir.resolve("docs/images/uma-logo-white.svg")}"
]
}
""".trimIndent(),
),
)
}
4 changes: 3 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ kotlin = "1.9.0"
kotlinCoroutines = "1.6.4"
kotlinxDateTime = "0.4.0"
kotlinSerializationJson = "1.4.1"
ktlint = "11.3.1"
kotlinReflect = "2.0.0"
ktlint = "12.1.1"
ktor = "2.2.3"
mavenPublish = "0.25.2"
mockitoCore = "5.5.0"
taskTree = "2.1.1"
junit = "4.13.2"
bitcoinj-core = "0.16.3"

[libraries]
gradleClasspath-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
Expand Down
1 change: 1 addition & 0 deletions uma-sdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ kotlin {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.ktor.client.core)
implementation(libs.jna)
implementation(kotlin("reflect"))
}
}
val commonTest by getting {
Expand Down
87 changes: 62 additions & 25 deletions uma-sdk/src/commonMain/kotlin/me/uma/UmaProtocolHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

package me.uma

import me.uma.crypto.Secp256k1
import me.uma.protocol.*
import me.uma.utils.isDomainLocalhost
import me.uma.utils.serialFormat
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import kotlin.math.roundToLong
Expand All @@ -18,10 +22,6 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.encodeToJsonElement
import me.uma.crypto.Secp256k1
import me.uma.protocol.*
import me.uma.utils.isDomainLocalhost
import me.uma.utils.serialFormat

/**
* A helper class for interacting with the UMA protocol. It provides methods for creating and verifying UMA requests
Expand Down Expand Up @@ -406,7 +406,12 @@ class UmaProtocolHelper @JvmOverloads constructor(
val unsignedCompliancePayerData = CompliancePayerData(
utxos = payerUtxos ?: emptyList(),
nodePubKey = payerNodePubKey,
encryptedTravelRuleInfo = travelRuleInfo?.let { encryptTravelRuleInfo(receiverEncryptionPubKey, it) },
encryptedTravelRuleInfo = travelRuleInfo?.let {
encryptTravelRuleInfo(
receiverEncryptionPubKey,
it,
)
},
kycStatus = payerKycStatus,
signature = "",
signatureNonce = nonce,
Expand All @@ -420,7 +425,10 @@ class UmaProtocolHelper @JvmOverloads constructor(
}

private fun encryptTravelRuleInfo(receiverEncryptionPubKey: ByteArray, travelRuleInfoJson: String): String {
return Secp256k1.encryptEcies(travelRuleInfoJson.encodeToByteArray(), receiverEncryptionPubKey).toHexString()
return Secp256k1.encryptEcies(
travelRuleInfoJson.encodeToByteArray(),
receiverEncryptionPubKey,
).toHexString()
}

/**
Expand All @@ -446,11 +454,7 @@ class UmaProtocolHelper @JvmOverloads constructor(
* @throws InvalidNonceException if the nonce has already been used/timestamp is too old.
*/
@Throws(InvalidNonceException::class)
fun verifyPayReqSignature(
payReq: PayRequest,
pubKeyResponse: PubKeyResponse,
nonceCache: NonceCache,
): Boolean {
fun verifyPayReqSignature(payReq: PayRequest, pubKeyResponse: PubKeyResponse, nonceCache: NonceCache): Boolean {
if (!payReq.isUmaRequest()) return false
val compliance = payReq.payerData?.compliance() ?: return false
nonceCache.checkAndSaveNonce(compliance.signatureNonce, compliance.signatureTimestamp)
Expand Down Expand Up @@ -715,9 +719,8 @@ class UmaProtocolHelper @JvmOverloads constructor(
),
)
}
val hasPaymentInfo = receivingCurrencyCode != null &&
receivingCurrencyDecimals != null &&
conversionRate != null
val hasPaymentInfo =
receivingCurrencyCode != null && receivingCurrencyDecimals != null && conversionRate != null
if (Version.parse(senderUmaVersion).major < 1) {
if (!hasPaymentInfo) {
throw IllegalArgumentException("Payment info is required for UMAv0")
Expand Down Expand Up @@ -852,7 +855,10 @@ class UmaProtocolHelper @JvmOverloads constructor(
pubKeyResponse: PubKeyResponse,
nonceCache: NonceCache,
): Boolean {
nonceCache.checkAndSaveNonce(postTransactionCallback.signatureNonce, postTransactionCallback.signatureTimestamp)
nonceCache.checkAndSaveNonce(
postTransactionCallback.signatureNonce,
postTransactionCallback.signatureTimestamp,
)
return verifySignature(
postTransactionCallback.signablePayload(),
postTransactionCallback.signature,
Expand Down Expand Up @@ -881,6 +887,45 @@ class UmaProtocolHelper @JvmOverloads constructor(
}
return identifier.substring(atIndex + 1)
}

/**
* Create an UMA invoice object
*/
fun getInvoice(
receiverUma: String,
invoiceUUID: String,
amount: Int,
receivingCurrency: InvoiceCurrency,
expiration: Int,
isSubjectToTravelRule: Boolean,
umaVersion: String,
commentCharsAllowed: Int? = null,
senderUma: String? = null,
invoiceLimit: Int? = null,
callback: String,
privateSigningKey: ByteArray,
kycStatus: KycStatus? = null,
requiredPayerData: CounterPartyDataOptions? = null,
): Invoice {
return Invoice(
receiverUma = receiverUma,
invoiceUUID = invoiceUUID,
amount = amount,
receivingCurrency = receivingCurrency,
expiration = expiration,
isSubjectToTravelRule = isSubjectToTravelRule,
umaVersion = umaVersion,
commentCharsAllowed = commentCharsAllowed,
senderUma = senderUma,
invoiceLimit = invoiceLimit,
callback = callback,
kycStatus = kycStatus,
requiredPayerData = requiredPayerData,
).apply {
val signedPayload = signPayload(toTLV(), privateSigningKey)
signature = signedPayload.toByteArray(Charsets.UTF_8)
}
}
}

interface UmaInvoiceCreator {
Expand All @@ -893,11 +938,7 @@ interface UmaInvoiceCreator {
* @return The encoded BOLT-11 invoice that should be returned to the sender for the given [PayRequest] wrapped in a
* [CompletableFuture].
*/
fun createUmaInvoice(
amountMsats: Long,
metadata: String,
receiverIdentifier: String?,
): CompletableFuture<String>
fun createUmaInvoice(amountMsats: Long, metadata: String, receiverIdentifier: String?): CompletableFuture<String>
}

interface SyncUmaInvoiceCreator {
Expand All @@ -911,9 +952,5 @@ interface SyncUmaInvoiceCreator {
* @param receiverIdentifier Optional identifier of the receiver.
* @return The encoded BOLT-11 invoice that should be returned to the sender for the given [PayRequest].
*/
fun createUmaInvoice(
amountMsats: Long,
metadata: String,
receiverIdentifier: String?,
): String
fun createUmaInvoice(amountMsats: Long, metadata: String, receiverIdentifier: String?): String
}
18 changes: 10 additions & 8 deletions uma-sdk/src/commonMain/kotlin/me/uma/UmaRequester.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ interface UmaRequester {
/**
* A Ktor-based implementation of [UmaRequester].
*/
class KtorUmaRequester @JvmOverloads constructor(private val client: HttpClient = HttpClient()) : UmaRequester {
override suspend fun makeGetRequest(url: String): String {
val response = client.get(url)
class KtorUmaRequester
@JvmOverloads
constructor(private val client: HttpClient = HttpClient()) : UmaRequester {
override suspend fun makeGetRequest(url: String): String {
val response = client.get(url)

if (response.status.isSuccess()) {
return response.bodyAsText()
} else {
throw Exception("Error making request: ${response.status}")
if (response.status.isSuccess()) {
return response.bodyAsText()
} else {
throw Exception("Error making request: ${response.status}")
}
}
}
}
24 changes: 13 additions & 11 deletions uma-sdk/src/commonMain/kotlin/me/uma/Version.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package me.uma

import me.uma.utils.serialFormat
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
import me.uma.utils.serialFormat

const val MAJOR_VERSION = 1
const val MINOR_VERSION = 0
Expand Down Expand Up @@ -58,20 +58,22 @@ class UnsupportedVersionException(
}

fun isVersionSupported(versionString: String): Boolean {
val version = try {
Version.parse(versionString)
} catch (e: IllegalArgumentException) {
return false
} catch (e: NumberFormatException) {
return false
}
val version =
try {
Version.parse(versionString)
} catch (e: IllegalArgumentException) {
return false
} catch (e: NumberFormatException) {
return false
}
return supportedMajorVersions().contains(version.major)
}

fun selectHighestSupportedVersion(otherVaspSupportedMajorVersions: List<Int>): String? {
val highestSupportedMajorVersion = otherVaspSupportedMajorVersions.filter {
supportedMajorVersions().contains(it)
}.maxOrNull() ?: return null
val highestSupportedMajorVersion =
otherVaspSupportedMajorVersions.filter {
supportedMajorVersions().contains(it)
}.maxOrNull() ?: return null
return getHighestSupportedVersionForMajorVersion(highestSupportedMajorVersion)
}

Expand Down
30 changes: 30 additions & 0 deletions uma-sdk/src/commonMain/kotlin/me/uma/crypto/Bech32.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package me.uma.crypto

/**
* wrapper class for bech32 conversion functions
*/
object Bech32 {
fun encodeBech32(hrp: String, data: ByteArray): String {
return me.uma.crypto.internal.encodeBech32(
hrp,
data.toByteList(),
)
}

fun decodeBech32(bech32str: String): Bech32Data {
val data = me.uma.crypto.internal.decodeBech32(bech32str)
return Bech32Data(
data.hrp,
data.data.toByteArray(),
)
}

private fun ByteArray.toByteList() = map { it.toUByte() }

private fun List<UByte>.toByteArray() = map { it.toByte() }.toByteArray()

data class Bech32Data(
val hrp: String,
val data: ByteArray,
)
}
7 changes: 4 additions & 3 deletions uma-sdk/src/commonMain/kotlin/me/uma/crypto/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ package me.uma.crypto
internal fun String.hexToByteArray(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }

val byteIterator = chunkedSequence(2)
.map { it.toInt(16).toByte() }
.iterator()
val byteIterator =
chunkedSequence(2)
.map { it.toInt(16).toByte() }
.iterator()

return ByteArray(length / 2) { byteIterator.next() }
}
Expand Down
Loading
Loading