diff --git a/library/build.gradle b/library/build.gradle index ee9a6a8..e65fb8c 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -16,6 +16,7 @@ configurations { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" implementation "org.bouncycastle:bcprov-jdk15on:$bouncycastleVersion" diff --git a/library/src/main/kotlin/one/mixin/bot/HttpClient.kt b/library/src/main/kotlin/one/mixin/bot/HttpClient.kt index 9f59eac..7fb26e1 100644 --- a/library/src/main/kotlin/one/mixin/bot/HttpClient.kt +++ b/library/src/main/kotlin/one/mixin/bot/HttpClient.kt @@ -16,7 +16,8 @@ import java.security.Security @Suppress("unused") class HttpClient private constructor( - val safeUser: SafeUser, + val safeUser: SafeUser?, + private val accessToken: String? = null, debug: Boolean = false, ) { init { @@ -24,15 +25,12 @@ class HttpClient private constructor( } private val okHttpClient: OkHttpClient by lazy { - createHttpClient(safeUser, false, debug) + createHttpClient(safeUser, accessToken, false, debug) } private val retrofit: Retrofit by lazy { - val builder = Retrofit.Builder() - .baseUrl(URL) - .addCallAdapterFactory(CoroutineCallAdapterFactory()) - .addConverterFactory(GsonConverterFactory.create()) - .client(okHttpClient) + val builder = Retrofit.Builder().baseUrl(URL).addCallAdapterFactory(CoroutineCallAdapterFactory()) + .addConverterFactory(GsonConverterFactory.create()).client(okHttpClient) builder.build() } @@ -91,8 +89,9 @@ class HttpClient private constructor( } class Builder { - private lateinit var safeUser: SafeUser + private var safeUser: SafeUser? = null private var debug: Boolean = false + private var accessToken: String? = null fun configSafeUser( userId: String, @@ -101,7 +100,13 @@ class HttpClient private constructor( serverPublicKey: ByteArray? = null, spendPrivateKey: ByteArray? = null, ): Builder { - safeUser = SafeUser(userId, sessionId, sessionPrivateKey.sliceArray(0..31), serverPublicKey, spendPrivateKey) + safeUser = + SafeUser(userId, sessionId, sessionPrivateKey.sliceArray(0..31), serverPublicKey, spendPrivateKey) + return this + } + + fun configAccessToken(accessToken: String): Builder { + this.accessToken = accessToken return this } @@ -111,7 +116,10 @@ class HttpClient private constructor( } fun build(): HttpClient { - return HttpClient(safeUser, debug) + require(!(safeUser == null && accessToken == null)) { "safeUser and accessToken can't be null at the same time" } + return HttpClient(safeUser, accessToken, debug) } } + + } diff --git a/library/src/main/kotlin/one/mixin/bot/api/call/UtxoCallService.kt b/library/src/main/kotlin/one/mixin/bot/api/call/UtxoCallService.kt index d86be77..bfa03e1 100644 --- a/library/src/main/kotlin/one/mixin/bot/api/call/UtxoCallService.kt +++ b/library/src/main/kotlin/one/mixin/bot/api/call/UtxoCallService.kt @@ -19,7 +19,7 @@ import retrofit2.http.Query interface UtxoCallService { @GET("safe/outputs") fun getOutputsCall( - @Query("members") members: String, + @Query("members") members: String?, @Query("threshold") threshold: Int, @Query("offset") offset: Long? = null, @Query("limit") limit: Int = 500, diff --git a/library/src/main/kotlin/one/mixin/bot/blaze/BlazeClient.kt b/library/src/main/kotlin/one/mixin/bot/blaze/BlazeClient.kt index acd4b7a..0cc2e21 100644 --- a/library/src/main/kotlin/one/mixin/bot/blaze/BlazeClient.kt +++ b/library/src/main/kotlin/one/mixin/bot/blaze/BlazeClient.kt @@ -30,7 +30,7 @@ class BlazeClient private constructor( private var reconnectInterval = 5000 private val okHttpClient: OkHttpClient by lazy { - createHttpClient(safeUser, true, debug) + createHttpClient(safeUser, null, true, debug) } private var webSocket: WebSocket? = null @@ -80,7 +80,8 @@ class BlazeClient private constructor( serverPublicKey: ByteArray? = null, spendPrivateKey: ByteArray? = null, ): Builder { - safeUser = SafeUser(userId, sessionId, sessionPrivateKey.sliceArray(0..31), serverPublicKey, spendPrivateKey) + safeUser = + SafeUser(userId, sessionId, sessionPrivateKey.sliceArray(0..31), serverPublicKey, spendPrivateKey) return this } @@ -124,8 +125,7 @@ class BlazeClient private constructor( } override fun onMessage(webSocket: WebSocket, bytes: ByteString) { - try { - // 消息通的时,重置连接次数 + try { // 消息通的时,重置连接次数 connectCount = 0 val blazeMsg = decodeAs(bytes, parseData) @@ -163,17 +163,26 @@ fun sendMsg(webSocket: WebSocket, action: Action, msgParam: MsgParam?): Boolean } fun sendTextMsg(webSocket: WebSocket, conversationId: String, recipientId: String, text: String): Boolean { - val msgParam = MsgParam(UUID.randomUUID().toString(), Category.PLAIN_TEXT.toString(), conversationId, recipientId, text) + val msgParam = + MsgParam(UUID.randomUUID().toString(), Category.PLAIN_TEXT.toString(), conversationId, recipientId, text) return sendMsg(webSocket, Action.CREATE_MESSAGE, msgParam) } fun sendCardMsg(webSocket: WebSocket, conversationId: String, recipientId: String, cards: Cards): Boolean { - val msgParam = MsgParam(UUID.randomUUID().toString(), Category.APP_CARD.toString(), conversationId, recipientId, Gson().toJson(cards)) + val msgParam = MsgParam( + UUID.randomUUID().toString(), Category.APP_CARD.toString(), conversationId, recipientId, Gson().toJson(cards) + ) return sendMsg(webSocket, Action.CREATE_MESSAGE, msgParam) } fun sendButtonsMsg(webSocket: WebSocket, conversationId: String, recipientId: String, buttons: List): Boolean { - val msgParam = MsgParam(UUID.randomUUID().toString(), Category.APP_BUTTON_GROUP.toString(), conversationId, recipientId, Gson().toJson(buttons)) + val msgParam = MsgParam( + UUID.randomUUID().toString(), + Category.APP_BUTTON_GROUP.toString(), + conversationId, + recipientId, + Gson().toJson(buttons) + ) return sendMsg(webSocket, Action.CREATE_MESSAGE, msgParam) } diff --git a/library/src/main/kotlin/one/mixin/bot/safe/Output.kt b/library/src/main/kotlin/one/mixin/bot/safe/Output.kt index c7755e5..9eda12f 100644 --- a/library/src/main/kotlin/one/mixin/bot/safe/Output.kt +++ b/library/src/main/kotlin/one/mixin/bot/safe/Output.kt @@ -7,7 +7,7 @@ import java.math.BigDecimal fun assetBalance( botClient: HttpClient, assetId: String, - members: List = listOf(botClient.safeUser.userId), + members: List = listOf(), ): String { val outputs = listUnspentOutputs( botClient, buildHashMembers(members), 1, assetId @@ -37,8 +37,9 @@ fun listOutputs( offset: Long, limit: Int, ): List { - val resp = - botClient.utxoService.getOutputsCall(membersHash, threshold, offset, limit, state, assetId).execute().body() + val resp = botClient.utxoService.getOutputsCall( + membersHash.ifEmpty { null }, threshold, offset, limit, state, assetId + ).execute().body() if (resp == null || !resp.isSuccess()) { throw SafeException("get safe/outputs ${resp?.error}") } diff --git a/library/src/main/kotlin/one/mixin/bot/safe/Transaction.kt b/library/src/main/kotlin/one/mixin/bot/safe/Transaction.kt index 0bbd49e..f14a5ad 100644 --- a/library/src/main/kotlin/one/mixin/bot/safe/Transaction.kt +++ b/library/src/main/kotlin/one/mixin/bot/safe/Transaction.kt @@ -36,6 +36,7 @@ fun sendTransactionToUser( memo: String?, traceId: String, ): List { + requireNotNull(botClient.safeUser) { "safe user is null" } verifyTxId(botClient, traceId) // check assetId is kernel assetId // get unspent outputs for asset and may throw insufficient outputs error @@ -122,6 +123,7 @@ suspend fun withdrawalToAddress( memo: String? = null, traceId: String = UUID.randomUUID().toString(), ): List { + requireNotNull(botClient.safeUser) { "safe user is null" } verifyTxId(botClient, traceId) val token = botClient.tokenService.getAssetById(assetId).requiredData() val chain = if (token.assetId == token.chainId) { @@ -245,7 +247,7 @@ private suspend fun HttpClient.withdrawalTransaction( val withdrawalData = requestResponse.first { it.requestId == traceId } val feeData = requestResponse.first { it.requestId == feeTraceId } - val spendKey = safeUser.spendPrivateKey ?: throw SafeException("spend key is null") + val spendKey = safeUser?.spendPrivateKey ?: throw SafeException("spend key is null") val signedWithdrawalRaw = withdrawalTx.sign(withdrawalData.views, utxos, spendKey.toHex()) val signedFeeRaw = feeTx.sign(feeData.views, feeUtxos, spendKey.toHex()) @@ -302,7 +304,7 @@ private suspend fun HttpClient.withdrawalTransaction( utxoService.transactionRequest(listOf(TransactionRequest(tx.encodeToString(), traceId))).requiredData() .firstOrNull() ?: throw SafeException("request transaction response data null") - val spendKey = safeUser.spendPrivateKey ?: throw SafeException("spend key is null") + val spendKey = safeUser?.spendPrivateKey ?: throw SafeException("spend key is null") val signedRaw = tx.sign(verifiedTx.views, utxos, spendKey.toHex()) utxoService.transactions(listOf(TransactionRequest(signedRaw, traceId))).requiredData() @@ -313,6 +315,7 @@ private fun requestUnspentOutputsForRecipients( assetId: String, amount: String, ): Pair, BigDecimal> { + requireNotNull(botClient.safeUser) { "safe user is null" } val memberHash = buildHashMembers(listOf(botClient.safeUser.userId)) val outputs = listUnspentOutputs(botClient, memberHash, 1, assetId) if (outputs.isEmpty()) { @@ -333,6 +336,9 @@ private fun requestUnspentOutputsForRecipients( } fun buildHashMembers(ids: List): String { + if (ids.isEmpty()) { + return "" + } return ids.sortedBy { it }.joinToString("").sha3Sum256().joinToString("") { "%02x".format(it) } } diff --git a/library/src/main/kotlin/one/mixin/bot/util/OkHttpProvider.kt b/library/src/main/kotlin/one/mixin/bot/util/OkHttpProvider.kt index 0e91d32..e72890e 100644 --- a/library/src/main/kotlin/one/mixin/bot/util/OkHttpProvider.kt +++ b/library/src/main/kotlin/one/mixin/bot/util/OkHttpProvider.kt @@ -15,10 +15,13 @@ import java.util.* import java.util.concurrent.TimeUnit fun createHttpClient( - safeUser: SafeUser, + safeUser: SafeUser?, + accessToken: String?, websocket: Boolean, debug: Boolean, ): OkHttpClient { + require(!(safeUser == null && accessToken == null)) { "safeUser and accessToken can't be null at the same time" } + val builder = OkHttpClient.Builder() if (debug) { val logging = HttpLoggingInterceptor() @@ -52,25 +55,24 @@ fun createHttpClient( if (websocket) { requestBuilder.addHeader("Sec-WebSocket-Protocol", "MixinBot-Blaze-1") } - requestBuilder.addHeader("User-Agent", Constants.UA).addHeader("Accept-Language", Locale.getDefault().language).addHeader( - "Authorization", - "Bearer " + - signToken( - safeUser.userId, + requestBuilder.addHeader("User-Agent", Constants.UA) + .addHeader("Accept-Language", Locale.getDefault().language).addHeader( + "Authorization", + "Bearer " + (accessToken ?: signToken( + safeUser!!.userId, safeUser.sessionId, chain.request(), EdDSAPrivateKey(safeUser.sessionPrivateKey.toByteString()), - ), - ) + )), + ) val request = requestBuilder.build() - val response = - try { - chain.proceed(request) - } catch (e: Exception) { - throw e - } + val response = try { + chain.proceed(request) + } catch (e: Exception) { + throw e + } if (!response.isSuccessful) { val code = response.code diff --git a/samples/build.gradle b/samples/build.gradle index 5ed8a3a..4d02d8c 100644 --- a/samples/build.gradle +++ b/samples/build.gradle @@ -21,4 +21,7 @@ dependencies { // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" + + implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" + } \ No newline at end of file