diff --git a/walletconnect/.gitignore b/walletconnect/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/walletconnect/.gitignore @@ -0,0 +1 @@ +/build diff --git a/walletconnect/build.gradle b/walletconnect/build.gradle new file mode 100644 index 0000000..d2a9d4a --- /dev/null +++ b/walletconnect/build.gradle @@ -0,0 +1,54 @@ +apply plugin: 'com.android.library' + +apply plugin: 'kotlin-android' + +group='com.github.TrustWallet' + +android { + compileSdkVersion 28 + + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + compileOptions { + sourceCompatibility = "1.8" + targetCompatibility = 1.8 + } + buildToolsVersion = '28.0.3' +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.squareup.okhttp3:okhttp:4.9.2' + implementation 'com.google.code.gson:gson:2.8.7' + implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' + implementation 'com.android.support:appcompat-v7:28.0.0' + testImplementation 'junit:junit:4.13' + testImplementation 'org.robolectric:robolectric:4.3' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} + +tasks.withType(Javadoc).all { enabled = false } +apply from: "$rootDir/publish/publish.gradle" diff --git a/walletconnect/library.properties b/walletconnect/library.properties new file mode 100644 index 0000000..5c14cac --- /dev/null +++ b/walletconnect/library.properties @@ -0,0 +1,4 @@ +artifact=walletconnect +libraryName=com.hipo.macaron +libraryVersion=1.0.0 +libraryDescription=WalletConnect Library that's been forked from TrustWallet diff --git a/walletconnect/proguard-rules.pro b/walletconnect/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/walletconnect/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/walletconnect/src/main/AndroidManifest.xml b/walletconnect/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8cf514c --- /dev/null +++ b/walletconnect/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/WCCipher.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/WCCipher.kt new file mode 100644 index 0000000..c2ad167 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/WCCipher.kt @@ -0,0 +1,73 @@ +package com.trustwallet.walletconnect + +import com.trustwallet.walletconnect.exceptions.InvalidHmacException +import com.trustwallet.walletconnect.extensions.hexStringToByteArray +import com.trustwallet.walletconnect.extensions.toHex +import com.trustwallet.walletconnect.models.WCEncryptionPayload +import java.security.SecureRandom +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +private val CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding" +private val MAC_ALGORITHM = "HmacSHA256" + +object WCCipher { + fun encrypt(data: ByteArray, key: ByteArray): WCEncryptionPayload { + val iv = randomBytes(16) + val keySpec = SecretKeySpec(key, "AES") + val ivSpec = IvParameterSpec(iv) + val cipher = Cipher.getInstance(CIPHER_ALGORITHM) + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) + + val encryptedData = cipher.doFinal(data) + val hmac = computeHmac( + data = encryptedData, + iv = iv, + key = key + ) + + return WCEncryptionPayload( + data = encryptedData.toHex(), + iv = iv.toHex(), + hmac = hmac + ) + } + + fun decrypt(payload: WCEncryptionPayload, key: ByteArray): ByteArray { + val data = payload.data.hexStringToByteArray() + val iv = payload.iv.hexStringToByteArray() + val computedHmac = computeHmac( + data = data, + iv = iv, + key = key + ) + + if (computedHmac != payload.hmac.toLowerCase()) { + throw InvalidHmacException() + } + + val keySpec = SecretKeySpec(key, "AES") + val ivSpec = IvParameterSpec(iv) + val cipher = Cipher.getInstance(CIPHER_ALGORITHM) + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) + + return cipher.doFinal(data) + } + + private fun computeHmac(data: ByteArray, iv: ByteArray, key: ByteArray): String { + val mac = Mac.getInstance(MAC_ALGORITHM) + val payload = data + iv + mac.init(SecretKeySpec(key, MAC_ALGORITHM)) + return mac.doFinal(payload).toHex() + } + + private fun randomBytes(size: Int): ByteArray { + val secureRandom = SecureRandom() + val bytes = ByteArray(size) + secureRandom.nextBytes(bytes) + + return bytes + } +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/WCClient.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/WCClient.kt new file mode 100644 index 0000000..0665673 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/WCClient.kt @@ -0,0 +1,392 @@ +package com.trustwallet.walletconnect + +import android.util.Log +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.registerTypeAdapter +import com.github.salomonbrys.kotson.typeToken +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.trustwallet.walletconnect.exceptions.InvalidJsonRpcParamsException +import com.trustwallet.walletconnect.extensions.hexStringToByteArray +import com.trustwallet.walletconnect.jsonrpc.JsonRpcError +import com.trustwallet.walletconnect.jsonrpc.JsonRpcErrorResponse +import com.trustwallet.walletconnect.jsonrpc.JsonRpcRequest +import com.trustwallet.walletconnect.jsonrpc.JsonRpcResponse +import com.trustwallet.walletconnect.models.* +import com.trustwallet.walletconnect.models.binance.* +import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage +import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction +import com.trustwallet.walletconnect.models.ethereum.ethTransactionSerializer +import com.trustwallet.walletconnect.models.session.WCApproveSessionResponse +import com.trustwallet.walletconnect.models.session.WCSession +import com.trustwallet.walletconnect.models.session.WCSessionRequest +import com.trustwallet.walletconnect.models.session.WCSessionUpdate +import okhttp3.* +import okio.ByteString +import java.util.* + +const val JSONRPC_VERSION = "2.0" +const val WS_CLOSE_NORMAL = 1000 + +open class WCClient ( + builder: GsonBuilder = GsonBuilder(), + private val httpClient: OkHttpClient +): WebSocketListener() { + private val TAG = "WCClient" + + private val gson = builder + .serializeNulls() + .registerTypeAdapter(cancelOrderSerializer) + .registerTypeAdapter(cancelOrderDeserializer) + .registerTypeAdapter(tradeOrderSerializer) + .registerTypeAdapter(tradeOrderDeserializer) + .registerTypeAdapter(transferOrderSerializer) + .registerTypeAdapter(transferOrderDeserializer) + .registerTypeAdapter(ethTransactionSerializer) + .create() + + private var socket: WebSocket? = null + + private val listeners: MutableSet = mutableSetOf() + + var session: WCSession? = null + private set + + var peerMeta: WCPeerMeta? = null + private set + + var peerId: String? = null + private set + + var remotePeerId: String? = null + private set + + var chainId: String? = null + private set + + var isConnected: Boolean = false + private set + + private var handshakeId: Long = -1 + + var onFailure: (Throwable) -> Unit = { _ -> Unit} + var onDisconnect: (code: Int, reason: String) -> Unit = { _, _ -> Unit } + var onSessionRequest: (id: Long, peer: WCPeerMeta) -> Unit = { _, _ -> Unit } + var onEthSign: (id: Long, message: WCEthereumSignMessage) -> Unit = { _, _ -> Unit } + var onEthSignTransaction: (id: Long, transaction: WCEthereumTransaction) -> Unit = { _, _ -> Unit } + var onEthSendTransaction: (id: Long, transaction: WCEthereumTransaction) -> Unit = { _, _ -> Unit } + var onCustomRequest: (id: Long, payload: String) -> Unit = { _, _ -> Unit } + var onBnbTrade: (id: Long, order: WCBinanceTradeOrder) -> Unit = { _, _ -> Unit } + var onBnbCancel: (id: Long, order: WCBinanceCancelOrder) -> Unit = { _, _ -> Unit } + var onBnbTransfer: (id: Long, order: WCBinanceTransferOrder) -> Unit = { _, _ -> Unit } + var onBnbTxConfirm: (id: Long, order: WCBinanceTxConfirmParam) -> Unit = { _, _ -> Unit } + var onGetAccounts: (id: Long) -> Unit = { _ -> Unit } + var onSignTransaction: (id: Long, transaction: WCSignTransaction) -> Unit = {_, _ -> Unit } + + override fun onOpen(webSocket: WebSocket, response: Response) { + Log.d(TAG, "<< websocket opened >>") + isConnected = true + + listeners.forEach { it.onOpen(webSocket, response) } + + val session = this.session ?: throw IllegalStateException("session can't be null on connection open") + val peerId = this.peerId ?: throw IllegalStateException("peerId can't be null on connection open") + // The Session.topic channel is used to listen session request messages only. + subscribe(session.topic) + // The peerId channel is used to listen to all messages sent to this httpClient. + subscribe(peerId) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + var decrypted: String? = null + try { + Log.d(TAG, "<== message $text") + decrypted = decryptMessage(text) + Log.d(TAG, "<== decrypted $decrypted") + handleMessage(decrypted) + } catch (e: Exception) { + onFailure(e) + } finally { + listeners.forEach { it.onMessage(webSocket, decrypted ?: text) } + } + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + resetState() + onFailure(t) + + listeners.forEach { it.onFailure(webSocket, t, response) } + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + Log.d(TAG,"<< websocket closed >>") + + listeners.forEach { it.onClosed(webSocket, code, reason) } + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + Log.d(TAG,"<== pong") + + listeners.forEach { it.onMessage(webSocket, bytes) } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + Log.d(TAG,"<< closing socket >>") + + resetState() + onDisconnect(code, reason) + + listeners.forEach { it.onClosing(webSocket, code, reason) } + } + + fun connect(session: WCSession, peerMeta: WCPeerMeta, peerId: String = UUID.randomUUID().toString(), remotePeerId: String? = null) { + if (this.session != null && this.session?.topic != session.topic) { + killSession() + } + + this.session = session + this.peerMeta = peerMeta + this.peerId = peerId + this.remotePeerId = remotePeerId + + val request = Request.Builder() + .url(session.bridge) + .build() + + socket = httpClient.newWebSocket(request, this) + } + + fun approveSession(accounts: List, chainId: Int): Boolean { + check(handshakeId > 0) { "handshakeId must be greater than 0 on session approve" } + + val result = WCApproveSessionResponse( + chainId = this.chainId?.toIntOrNull() ?: chainId, + accounts = accounts, + peerId = peerId, + peerMeta = peerMeta + ) + val response = JsonRpcResponse( + id = handshakeId, + result = result + ) + + return encryptAndSend(gson.toJson(response)) + } + + fun updateSession(accounts: List? = null, chainId: Int? = null, approved: Boolean = true): Boolean { + val request = JsonRpcRequest( + id = generateId(), + method = WCMethod.SESSION_UPDATE, + params = listOf( + WCSessionUpdate( + approved = approved, + chainId = this.chainId?.toIntOrNull() ?: chainId, + accounts = accounts + ) + ) + ) + return encryptAndSend(gson.toJson(request)) + } + + fun rejectSession(message: String = "Session rejected"): Boolean { + check(handshakeId > 0) { "handshakeId must be greater than 0 on session reject" } + + val response = JsonRpcErrorResponse( + id = handshakeId, + error = JsonRpcError.serverError( + message = message + ) + ) + return encryptAndSend(gson.toJson(response)) + } + + fun killSession(): Boolean { + updateSession(approved = false) + return disconnect() + } + + fun approveRequest(id: Long, result: T): Boolean { + val response = JsonRpcResponse( + id = id, + result = result + ) + return encryptAndSend(gson.toJson(response)) + } + + fun rejectRequest(id: Long, message: String = "Reject by the user"): Boolean { + val response = JsonRpcErrorResponse( + id = id, + error = JsonRpcError.serverError( + message = message + ) + ) + return encryptAndSend(gson.toJson(response)) + } + + private fun decryptMessage(text: String): String { + val message = gson.fromJson(text) + val encrypted = gson.fromJson(message.payload) + val session = this.session ?: throw IllegalStateException("session can't be null on message receive") + return String(WCCipher.decrypt(encrypted, session.key.hexStringToByteArray()), Charsets.UTF_8) + } + + private fun invalidParams(id: Long): Boolean { + val response = JsonRpcErrorResponse( + id = id, + error = JsonRpcError.invalidParams( + message = "Invalid parameters" + ) + ) + + return encryptAndSend(gson.toJson(response)) + } + + private fun handleMessage(payload: String) { + try { + val request = gson.fromJson>(payload, typeToken>()) + val method = request.method + if (method != null) { + handleRequest(request) + } else { + onCustomRequest(request.id, payload) + } + } catch (e: InvalidJsonRpcParamsException) { + invalidParams(e.requestId) + } + } + + private fun handleRequest(request: JsonRpcRequest) { + when (request.method) { + WCMethod.SESSION_REQUEST -> { + val param = gson.fromJson>(request.params) + .firstOrNull() ?: throw InvalidJsonRpcParamsException(request.id) + handshakeId = request.id + remotePeerId = param.peerId + chainId = param.chainId + onSessionRequest(request.id, param.peerMeta) + } + WCMethod.SESSION_UPDATE -> { + val param = gson.fromJson>(request.params) + .firstOrNull() ?: throw InvalidJsonRpcParamsException(request.id) + if (!param.approved) { + killSession() + } + } + WCMethod.ETH_SIGN -> { + val params = gson.fromJson>(request.params) + if (params.size < 2) + throw InvalidJsonRpcParamsException(request.id) + onEthSign(request.id, WCEthereumSignMessage(params, WCEthereumSignMessage.WCSignType.MESSAGE)) + } + WCMethod.ETH_PERSONAL_SIGN -> { + val params = gson.fromJson>(request.params) + if (params.size < 2) + throw InvalidJsonRpcParamsException(request.id) + onEthSign(request.id, WCEthereumSignMessage(params, WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE)) + } + WCMethod.ETH_SIGN_TYPE_DATA -> { + val params = gson.fromJson>(request.params) + if (params.size < 2) + throw InvalidJsonRpcParamsException(request.id) + onEthSign(request.id, WCEthereumSignMessage(params, WCEthereumSignMessage.WCSignType.TYPED_MESSAGE)) + } + WCMethod.ETH_SIGN_TRANSACTION -> { + val param = gson.fromJson>(request.params) + .firstOrNull() ?: throw InvalidJsonRpcParamsException(request.id) + onEthSignTransaction(request.id, param) + } + WCMethod.ETH_SEND_TRANSACTION ->{ + val param = gson.fromJson>(request.params) + .firstOrNull() ?: throw InvalidJsonRpcParamsException(request.id) + onEthSendTransaction(request.id, param) + } + WCMethod.BNB_SIGN -> { + try { + val order = gson.fromJson>(request.params) + .firstOrNull() ?: throw InvalidJsonRpcParamsException(request.id) + onBnbCancel(request.id, order) + } catch (e: NoSuchElementException) { } + + try { + val order = gson.fromJson>(request.params) + .firstOrNull() ?: throw InvalidJsonRpcParamsException(request.id) + onBnbTrade(request.id, order) + } catch (e: NoSuchElementException) { } + + try { + val order = gson.fromJson>(request.params) + .firstOrNull() ?: throw InvalidJsonRpcParamsException(request.id) + onBnbTransfer(request.id, order) + } catch (e: NoSuchElementException) { } + } + WCMethod.BNB_TRANSACTION_CONFIRM -> { + val param = gson.fromJson>(request.params) + .firstOrNull() ?: throw InvalidJsonRpcParamsException(request.id) + onBnbTxConfirm(request.id, param) + } + WCMethod.GET_ACCOUNTS -> { + onGetAccounts(request.id) + } + WCMethod.SIGN_TRANSACTION -> { + val param = gson.fromJson>(request.params) + .firstOrNull() ?: throw InvalidJsonRpcParamsException(request.id) + onSignTransaction(request.id, param) + } + } + } + + private fun subscribe(topic: String): Boolean { + val message = WCSocketMessage( + topic = topic, + type = MessageType.SUB, + payload = "" + ) + val json = gson.toJson(message) + Log.d(TAG,"==> subscribe $json") + + return socket?.send(gson.toJson(message)) ?: false + } + + private fun encryptAndSend(result: String): Boolean { + Log.d(TAG,"==> message $result") + val session = this.session ?: throw IllegalStateException("session can't be null on message send") + val payload = gson.toJson(WCCipher.encrypt(result.toByteArray(Charsets.UTF_8), session.key.hexStringToByteArray())) + val message = WCSocketMessage( + // Once the remotePeerId is defined, all messages must be sent to this channel. The session.topic channel + // will be used only to respond the session request message. + topic = remotePeerId ?: session.topic, + type = MessageType.PUB, + payload = payload + ) + val json = gson.toJson(message) + Log.d(TAG,"==> encrypted $json") + + return socket?.send(json) ?: false + } + + + fun disconnect(): Boolean { + return socket?.close(WS_CLOSE_NORMAL, null) ?: false + } + + fun addSocketListener(listener: WebSocketListener) { + listeners.add(listener) + } + + fun removeSocketListener(listener: WebSocketListener) { + listeners.remove(listener) + } + + private fun resetState() { + handshakeId = -1 + isConnected = false + session = null + peerId = null + remotePeerId = null + peerMeta = null + } +} + +private fun generateId(): Long { + return Date().time +} diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/WCSessionStore.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/WCSessionStore.kt new file mode 100644 index 0000000..c711851 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/WCSessionStore.kt @@ -0,0 +1,19 @@ +package com.trustwallet.walletconnect + +import com.trustwallet.walletconnect.models.WCPeerMeta +import com.trustwallet.walletconnect.models.session.WCSession +import java.util.* + +data class WCSessionStoreItem( + val session: WCSession, + val chainId: Int, + val peerId: String, + val remotePeerId: String, + val remotePeerMeta: WCPeerMeta, + val isAutoSign: Boolean = false, + val date: Date = Date() +) + +interface WCSessionStore { + var session: WCSessionStoreItem? +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/WCSessionStoreType.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/WCSessionStoreType.kt new file mode 100644 index 0000000..e988840 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/WCSessionStoreType.kt @@ -0,0 +1,35 @@ +package com.trustwallet.walletconnect + +import android.content.SharedPreferences +import com.github.salomonbrys.kotson.* +import com.google.gson.GsonBuilder + +class WCSessionStoreType( + private val sharedPreferences: SharedPreferences, + builder: GsonBuilder = GsonBuilder() +): WCSessionStore { + private val gson = builder + .serializeNulls() + .create() + + private fun store(item: WCSessionStoreItem?) { + if (item != null) { + sharedPreferences.edit().putString(SESSION_KEY, gson.toJson(item)).apply() + } else { + sharedPreferences.edit().remove(SESSION_KEY).apply() + } + } + + private fun load(): WCSessionStoreItem? { + val json = sharedPreferences.getString(SESSION_KEY, null) ?: return null + return gson.fromJson(json) + } + + override var session: WCSessionStoreItem? + set(item) = store(item) + get() = load() + + companion object { + private const val SESSION_KEY = "org.walletconnect.session" + } +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidHmacException.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidHmacException.kt new file mode 100644 index 0000000..f6215f8 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidHmacException.kt @@ -0,0 +1,5 @@ +package com.trustwallet.walletconnect.exceptions + +import java.lang.Exception + +class InvalidHmacException : Exception("Received and computed HMAC doesn't mach") \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidJsonRpcParamsException.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidJsonRpcParamsException.kt new file mode 100644 index 0000000..1624b4d --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidJsonRpcParamsException.kt @@ -0,0 +1,5 @@ +package com.trustwallet.walletconnect.exceptions + +import java.lang.Exception + +class InvalidJsonRpcParamsException(val requestId: Long) : Exception("Invalid JSON RPC Request") diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidMessageException.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidMessageException.kt new file mode 100644 index 0000000..9888268 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidMessageException.kt @@ -0,0 +1,5 @@ +package com.trustwallet.walletconnect.exceptions + +import java.lang.Exception + +class InvalidMessageException : Exception("Invalid message") \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidPayloadException.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidPayloadException.kt new file mode 100644 index 0000000..9805467 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidPayloadException.kt @@ -0,0 +1,5 @@ +package com.trustwallet.walletconnect.exceptions + +import java.lang.Exception + +class InvalidPayloadException : Exception("Invalid WCEncryptionPayload") \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidSessionException.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidSessionException.kt new file mode 100644 index 0000000..8c38512 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidSessionException.kt @@ -0,0 +1,5 @@ +package com.trustwallet.walletconnect.exceptions + +import java.lang.Exception + +class InvalidSessionException : Exception("Invalid session") \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidUriException.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidUriException.kt new file mode 100644 index 0000000..8d3fb55 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/InvalidUriException.kt @@ -0,0 +1,5 @@ +package com.trustwallet.walletconnect.exceptions + +import java.lang.Exception + +class InvalidUriException : Exception("Invalid Wallet Connect URI") \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/RequiredFieldException.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/RequiredFieldException.kt new file mode 100644 index 0000000..093a5b8 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/exceptions/RequiredFieldException.kt @@ -0,0 +1,5 @@ +package com.trustwallet.walletconnect.exceptions + +import com.google.gson.JsonParseException + +class RequiredFieldException(val field: String = "") : JsonParseException("'$field' is required") \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/extensions/ByteArray.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/extensions/ByteArray.kt new file mode 100644 index 0000000..e2797d8 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/extensions/ByteArray.kt @@ -0,0 +1,17 @@ +package com.trustwallet.walletconnect.extensions + +private val HEX_CHARS = "0123456789abcdef".toCharArray() + +fun ByteArray.toHex() : String{ + val result = StringBuffer() + + forEach { + val octet = it.toInt() + val firstIndex = (octet and 0xF0).ushr(4) + val secondIndex = octet and 0x0F + result.append(HEX_CHARS[firstIndex]) + result.append(HEX_CHARS[secondIndex]) + } + + return result.toString() +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/extensions/String.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/extensions/String.kt new file mode 100644 index 0000000..88e4d8b --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/extensions/String.kt @@ -0,0 +1,18 @@ +package com.trustwallet.walletconnect.extensions + +private val HEX_CHARS = "0123456789abcdef" + +fun String.hexStringToByteArray() : ByteArray { + val hex = toLowerCase() + val result = ByteArray(length / 2) + + for (i in 0 until hex.length step 2) { + val firstIndex = HEX_CHARS.indexOf(hex[i]) + val secondIndex = HEX_CHARS.indexOf(hex[i + 1]) + + val octet = firstIndex.shl(4).or(secondIndex) + result.set(i.shr(1), octet.toByte()) + } + + return result +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/jsonrpc/JsonRpcError.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/jsonrpc/JsonRpcError.kt new file mode 100644 index 0000000..d87a7d7 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/jsonrpc/JsonRpcError.kt @@ -0,0 +1,14 @@ +package com.trustwallet.walletconnect.jsonrpc + +data class JsonRpcError ( + val code: Int, + val message: String +) { + companion object { + fun serverError(message: String) = JsonRpcError(-32000, message) + fun invalidParams(message: String) = JsonRpcError(-32602, message) + fun invalidRequest(message: String) = JsonRpcError(-32600, message) + fun parseError(message: String) = JsonRpcError(-32700, message) + fun methodNotFound(message: String) = JsonRpcError(-32601, message) + } +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/jsonrpc/JsonRpcRequest.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/jsonrpc/JsonRpcRequest.kt new file mode 100644 index 0000000..5a46c73 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/jsonrpc/JsonRpcRequest.kt @@ -0,0 +1,11 @@ +package com.trustwallet.walletconnect.jsonrpc + +import com.trustwallet.walletconnect.JSONRPC_VERSION +import com.trustwallet.walletconnect.models.WCMethod + +data class JsonRpcRequest( + val id: Long, + val jsonrpc: String = JSONRPC_VERSION, + val method: WCMethod?, + val params: T +) \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/jsonrpc/JsonRpcResponse.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/jsonrpc/JsonRpcResponse.kt new file mode 100644 index 0000000..55a2ea1 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/jsonrpc/JsonRpcResponse.kt @@ -0,0 +1,15 @@ +package com.trustwallet.walletconnect.jsonrpc + +import com.trustwallet.walletconnect.JSONRPC_VERSION + +data class JsonRpcResponse ( + val jsonrpc: String = JSONRPC_VERSION, + val id: Long, + val result: T +) + +data class JsonRpcErrorResponse ( + val jsonrpc: String = JSONRPC_VERSION, + val id: Long, + val error: JsonRpcError +) \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/MessageType.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/MessageType.kt new file mode 100644 index 0000000..179c76e --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/MessageType.kt @@ -0,0 +1,8 @@ +package com.trustwallet.walletconnect.models + +import com.google.gson.annotations.SerializedName + +enum class MessageType { + @SerializedName("pub") PUB, + @SerializedName("sub") SUB +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCAccount.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCAccount.kt new file mode 100644 index 0000000..d99d839 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCAccount.kt @@ -0,0 +1,6 @@ +package com.trustwallet.walletconnect.models + +data class WCAccount( + val network: Int, + val address: String +) \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCEncryptionPayload.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCEncryptionPayload.kt new file mode 100644 index 0000000..fdd71c6 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCEncryptionPayload.kt @@ -0,0 +1,7 @@ +package com.trustwallet.walletconnect.models + +data class WCEncryptionPayload( + val data: String, + val hmac: String, + val iv: String +) \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCMethod.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCMethod.kt new file mode 100644 index 0000000..5880bfd --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCMethod.kt @@ -0,0 +1,38 @@ +package com.trustwallet.walletconnect.models + +import com.google.gson.annotations.SerializedName + +enum class WCMethod { + @SerializedName("wc_sessionRequest") + SESSION_REQUEST, + + @SerializedName("wc_sessionUpdate") + SESSION_UPDATE, + + @SerializedName("eth_sign") + ETH_SIGN, + + @SerializedName("personal_sign") + ETH_PERSONAL_SIGN, + + @SerializedName("eth_signTypedData") + ETH_SIGN_TYPE_DATA, + + @SerializedName("eth_signTransaction") + ETH_SIGN_TRANSACTION, + + @SerializedName("eth_sendTransaction") + ETH_SEND_TRANSACTION, + + @SerializedName("bnb_sign") + BNB_SIGN, + + @SerializedName("bnb_tx_confirmation") + BNB_TRANSACTION_CONFIRM, + + @SerializedName("get_accounts") + GET_ACCOUNTS, + + @SerializedName("trust_signTransaction") + SIGN_TRANSACTION; +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCPeerMeta.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCPeerMeta.kt new file mode 100644 index 0000000..1682648 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCPeerMeta.kt @@ -0,0 +1,8 @@ +package com.trustwallet.walletconnect.models + +data class WCPeerMeta ( + val name: String, + val url: String, + val description: String? = null, + val icons: List = listOf("") +) \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCSignTransaction.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCSignTransaction.kt new file mode 100644 index 0000000..54aba46 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCSignTransaction.kt @@ -0,0 +1,6 @@ +package com.trustwallet.walletconnect.models + +data class WCSignTransaction( + val network: Int, + val transaction: String +) \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCSocketMessage.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCSocketMessage.kt new file mode 100644 index 0000000..edbacdc --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/WCSocketMessage.kt @@ -0,0 +1,7 @@ +package com.trustwallet.walletconnect.models + +data class WCSocketMessage( + val topic: String, + val type: MessageType, + val payload: String +) \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceCancelOrder.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceCancelOrder.kt new file mode 100644 index 0000000..19bddf5 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceCancelOrder.kt @@ -0,0 +1,46 @@ +package com.trustwallet.walletconnect.models.binance + +import com.github.salomonbrys.kotson.get +import com.github.salomonbrys.kotson.jsonDeserializer +import com.github.salomonbrys.kotson.jsonSerializer +import com.github.salomonbrys.kotson.string +import com.google.gson.JsonObject + +class WCBinanceCancelOrder( + account_number: String, + chain_id: String, + data: String?, + memo: String?, + sequence: String, + source: String, + msgs: List +): WCBinanceOrder(account_number, chain_id, data, memo, sequence, source, msgs) { + + enum class MessageKey(val key: String) { + REFID("refid"), + SENDER("sender"), + SYMBOL("symbol") + } + + data class Message( + val refid: String, + val sender: String, + val symbol: String + ) +} + +val cancelOrderDeserializer = jsonDeserializer { + WCBinanceCancelOrder.Message( + refid = it.json[WCBinanceCancelOrder.MessageKey.REFID.key].string, + sender = it.json[WCBinanceCancelOrder.MessageKey.SENDER.key].string, + symbol = it.json[WCBinanceCancelOrder.MessageKey.SYMBOL.key].string + ) +} + +val cancelOrderSerializer = jsonSerializer { + val jsonObject = JsonObject() + jsonObject.addProperty("refid", it.src.refid) + jsonObject.addProperty("sender", it.src.sender) + jsonObject.addProperty("symbol", it.src.symbol) + jsonObject +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceOrder.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceOrder.kt new file mode 100644 index 0000000..291ba2b --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceOrder.kt @@ -0,0 +1,20 @@ +package com.trustwallet.walletconnect.models.binance + +import com.google.gson.annotations.SerializedName + +open class WCBinanceOrder( + @SerializedName("account_number") + val accountNumber: String, + @SerializedName("chain_id") + val chainId: String, + val data: String?, + val memo: String?, + val sequence: String, + val source: String, + val msgs: List +) + +data class WCBinanceTxConfirmParam( + val ok: Boolean, + val errorMsg: String? +) diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceTradeOrder.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceTradeOrder.kt new file mode 100644 index 0000000..3a29cd7 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceTradeOrder.kt @@ -0,0 +1,64 @@ +package com.trustwallet.walletconnect.models.binance + +import com.github.salomonbrys.kotson.* +import com.google.gson.JsonObject + +class WCBinanceTradeOrder( + account_number: String, + chain_id: String, + data: String?, + memo: String?, + sequence: String, + source: String, + msgs: List +) : WCBinanceOrder (account_number, chain_id, data, memo, sequence, source, msgs) { + + enum class MessageKey(val key: String) { + ID("id"), + ORDER_TYPE("ordertype"), + PRICE("price"), + QUANTITY("quantity"), + SENDER("sender"), + SIDE("side"), + SYMBOL("symbol"), + TIME_INFORCE("timeinforce") + } + + data class Message( + val id: String, + val orderType: Int, + val price: Long, + val quantity: Long, + val sender: String, + val side: Int, + val symbol: String, + val timeInforce: Int + ) +} + +val tradeOrderDeserializer = jsonDeserializer { + WCBinanceTradeOrder.Message( + id = it.json[WCBinanceTradeOrder.MessageKey.ID.key].string, + orderType = it.json[WCBinanceTradeOrder.MessageKey.ORDER_TYPE.key].int, + price = it.json[WCBinanceTradeOrder.MessageKey.PRICE.key].long, + quantity = it.json[WCBinanceTradeOrder.MessageKey.QUANTITY.key].long, + sender = it.json[WCBinanceTradeOrder.MessageKey.SENDER.key].string, + side = it.json[WCBinanceTradeOrder.MessageKey.SIDE.key].int, + symbol = it.json[WCBinanceTradeOrder.MessageKey.SYMBOL.key].string, + timeInforce = it.json[WCBinanceTradeOrder.MessageKey.TIME_INFORCE.key].int + ) +} + +val tradeOrderSerializer = jsonSerializer { + val jsonObject = JsonObject() + jsonObject.addProperty(WCBinanceTradeOrder.MessageKey.ID.key, it.src.id) + jsonObject.addProperty(WCBinanceTradeOrder.MessageKey.ORDER_TYPE.key, it.src.orderType) + jsonObject.addProperty(WCBinanceTradeOrder.MessageKey.PRICE.key, it.src.price) + jsonObject.addProperty(WCBinanceTradeOrder.MessageKey.QUANTITY.key, it.src.quantity) + jsonObject.addProperty(WCBinanceTradeOrder.MessageKey.SENDER.key, it.src.sender) + jsonObject.addProperty(WCBinanceTradeOrder.MessageKey.SIDE.key, it.src.side) + jsonObject.addProperty(WCBinanceTradeOrder.MessageKey.SYMBOL.key, it.src.symbol) + jsonObject.addProperty(WCBinanceTradeOrder.MessageKey.TIME_INFORCE.key, it.src.timeInforce) + + jsonObject +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceTradePair.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceTradePair.kt new file mode 100644 index 0000000..c8246c9 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceTradePair.kt @@ -0,0 +1,17 @@ +package com.trustwallet.walletconnect.models.binance + +data class WCBinanceTradePair(val from: String, val to: String) { + companion object { + fun from(symbol: String): WCBinanceTradePair? { + val pair = symbol.split("_") + + return if (pair.size > 1) { + val firstParts = pair[0].split("-") + val secondParts = pair[1].split("-") + WCBinanceTradePair(firstParts[0], secondParts[0]) + } else { + null + } + } + } +} diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceTransferOrder.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceTransferOrder.kt new file mode 100644 index 0000000..8c8a75b --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/binance/WCBinanceTransferOrder.kt @@ -0,0 +1,51 @@ +package com.trustwallet.walletconnect.models.binance + +import com.github.salomonbrys.kotson.* +import com.google.gson.JsonObject + +class WCBinanceTransferOrder( + account_number: String, + chain_id: String, + data: String?, + memo: String?, + sequence: String, + source: String, + msgs: List +): WCBinanceOrder(account_number, chain_id, data, memo, sequence, source, msgs) { + + enum class MessageKey(val key: String) { + INPUTS("inputs"), + OUTPUTS("outputs") + } + + data class Message( + val inputs: List, + val outputs: List + ) { + + data class Item( + val address: String, + val coins: List + ) { + + data class Coin( + val amount: Long, + val denom: String + ) + } + } +} + +val transferOrderDeserializer = jsonDeserializer { + WCBinanceTransferOrder.Message( + inputs = it.context.deserialize(it.json[WCBinanceTransferOrder.MessageKey.INPUTS.key].array), + outputs = it.context.deserialize(it.json[WCBinanceTransferOrder.MessageKey.OUTPUTS.key].array) + ) +} + +val transferOrderSerializer = jsonSerializer { + val jsonObject = JsonObject() + jsonObject.addProperty(WCBinanceTransferOrder.MessageKey.INPUTS.key, it.context.serialize(it.src.inputs)) + jsonObject.addProperty(WCBinanceTransferOrder.MessageKey.OUTPUTS.key, it.context.serialize(it.src.outputs)) + jsonObject +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/ethereum/WCEthereumSignMessage.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/ethereum/WCEthereumSignMessage.kt new file mode 100644 index 0000000..642337b --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/ethereum/WCEthereumSignMessage.kt @@ -0,0 +1,26 @@ +package com.trustwallet.walletconnect.models.ethereum + +data class WCEthereumSignMessage ( + val raw: List, + val type: WCSignType +) { + enum class WCSignType { + MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE + } + + /** + * Raw parameters will always be the message and the addess. Depending on the WCSignType, + * those parameters can be swapped as description below: + * + * - MESSAGE: `[address, data ]` + * - TYPED_MESSAGE: `[address, data]` + * - PERSONAL_MESSAGE: `[data, address]` + * + * reference: https://docs.walletconnect.org/json-rpc/ethereum#eth_signtypeddata + */ + val data get() = when (type) { + WCSignType.MESSAGE -> raw[1] + WCSignType.TYPED_MESSAGE -> raw[1] + WCSignType.PERSONAL_MESSAGE -> raw[0] + } +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/ethereum/WCEthereumTransaction.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/ethereum/WCEthereumTransaction.kt new file mode 100644 index 0000000..a7c1909 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/ethereum/WCEthereumTransaction.kt @@ -0,0 +1,24 @@ +package com.trustwallet.walletconnect.models.ethereum + +import com.github.salomonbrys.kotson.jsonDeserializer + +data class WCEthereumTransaction( + val from: String, + val to: String?, + val nonce: String?, + val gasPrice: String?, + val gas: String?, + val gasLimit: String?, + val value: String?, + val data: String +) + +val ethTransactionSerializer = jsonDeserializer> { + val array = mutableListOf() + it.json.asJsonArray.forEach { tx -> + if (tx.isJsonObject) { + array.add(it.context.deserialize(tx)) + } + } + array +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCApproveSessionResponse.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCApproveSessionResponse.kt new file mode 100644 index 0000000..ecf2db7 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCApproveSessionResponse.kt @@ -0,0 +1,11 @@ +package com.trustwallet.walletconnect.models.session + +import com.trustwallet.walletconnect.models.WCPeerMeta + +data class WCApproveSessionResponse( + val approved: Boolean = true, + val chainId: Int, + val accounts: List, + val peerId: String?, + val peerMeta: WCPeerMeta? +) \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCSession.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCSession.kt new file mode 100644 index 0000000..e39c71d --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCSession.kt @@ -0,0 +1,33 @@ +package com.trustwallet.walletconnect.models.session + +import android.net.Uri + +data class WCSession ( + val topic: String, + val version: String, + val bridge: String, + val key: String +) { + fun toUri(): String = "wc:${topic}@${version}?bridge=${bridge}&key=${key}" + + companion object { + fun from(from: String): WCSession? { + if (!from.startsWith("wc:")) { + return null + } + + val uriString = from.replace("wc:", "wc://") + val uri = Uri.parse(uriString) + val bridge = uri.getQueryParameter("bridge") + val key = uri.getQueryParameter("key") + val topic = uri.userInfo + val version = uri.host + + if (bridge == null || key == null || topic == null || version == null) { + return null + } + + return WCSession(topic, version, bridge, key) + } + } +} \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCSessionRequest.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCSessionRequest.kt new file mode 100644 index 0000000..875e479 --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCSessionRequest.kt @@ -0,0 +1,9 @@ +package com.trustwallet.walletconnect.models.session + +import com.trustwallet.walletconnect.models.WCPeerMeta + +data class WCSessionRequest( + val peerId: String, + val peerMeta: WCPeerMeta, + val chainId: String? +) \ No newline at end of file diff --git a/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCSessionUpdate.kt b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCSessionUpdate.kt new file mode 100644 index 0000000..3c1137a --- /dev/null +++ b/walletconnect/src/main/java/com/trustwallet/walletconnect/models/session/WCSessionUpdate.kt @@ -0,0 +1,7 @@ +package com.trustwallet.walletconnect.models.session + +data class WCSessionUpdate( + val approved: Boolean, + val chainId: Int?, + val accounts: List? +) \ No newline at end of file diff --git a/walletconnect/src/main/res/values/strings.xml b/walletconnect/src/main/res/values/strings.xml new file mode 100644 index 0000000..eb53a04 --- /dev/null +++ b/walletconnect/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + WalletConnect + diff --git a/walletconnect/src/test/java/com/trustwallet/walletconnect/WCCipherTests.kt b/walletconnect/src/test/java/com/trustwallet/walletconnect/WCCipherTests.kt new file mode 100644 index 0000000..d5c91be --- /dev/null +++ b/walletconnect/src/test/java/com/trustwallet/walletconnect/WCCipherTests.kt @@ -0,0 +1,40 @@ +package com.trustwallet.walletconnect + +import android.os.Build +import com.trustwallet.walletconnect.extensions.hexStringToByteArray +import com.trustwallet.walletconnect.models.WCEncryptionPayload +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.O_MR1]) +class WCCipherTests { + @Test + fun test_decrypt() { + val data = "1b3db3674de082d65455eba0ae61cfe7e681c8ef1132e60c8dbd8e52daf18f4fea42cc76366c83351dab6dca52682ff81f828753f89a21e1cc46587ca51ccd353914ffdd3b0394acfee392be6c22b3db9237d3f717a3777e3577dd70408c089a4c9c85130a68c43b0a8aadb00f1b8a8558798104e67aa4ff027b35d4b989e7fd3988d5dcdd563105767670be735b21c4" + val hmac = "a33f868e793ca4fcca964bcb64430f65e2f1ca7a779febeaf94c5373d6df48b3" + val iv = "89ef1d6728bac2f1dcde2ef9330d2bb8" + val key = "5caa3a74154cee16bd1b570a1330be46e086474ac2f4720530662ef1a469662c".hexStringToByteArray() + val payload = WCEncryptionPayload( + data = data, + iv = iv, + hmac = hmac + ) + + val decrypted = String(WCCipher.decrypt(payload, key), Charsets.UTF_8) + val expected = "{\"id\":1554098597199736,\"jsonrpc\":\"2.0\",\"method\":\"wc_sessionUpdate\",\"params\":[{\"approved\":false,\"chainId\":null,\"accounts\":null}]}" + Assert.assertEquals(expected, decrypted) + } + + @Test + fun test_encrypt() { + val expected = "{\"id\":1554098597199736,\"jsonrpc\":\"2.0\",\"method\":\"wc_sessionUpdate\",\"params\":[{\"approved\":false,\"chainId\":null,\"accounts\":null}]}".hexStringToByteArray() + val key = "5caa3a74154cee16bd1b570a1330be46e086474ac2f4720530662ef1a469662c".hexStringToByteArray() + val payload = WCCipher.encrypt(data = expected, key = key) + val decrypted = WCCipher.decrypt(payload, key) + Assert.assertArrayEquals(expected, decrypted) + } +} \ No newline at end of file diff --git a/walletconnect/src/test/java/com/trustwallet/walletconnect/WCSessionStoreTests.kt b/walletconnect/src/test/java/com/trustwallet/walletconnect/WCSessionStoreTests.kt new file mode 100644 index 0000000..067cfc4 --- /dev/null +++ b/walletconnect/src/test/java/com/trustwallet/walletconnect/WCSessionStoreTests.kt @@ -0,0 +1,51 @@ +package com.trustwallet.walletconnect + +import android.content.Context +import android.os.Build +import com.trustwallet.walletconnect.models.WCPeerMeta +import com.trustwallet.walletconnect.models.session.WCSession +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.M]) +class WCSessionStoreTests { + private val context = RuntimeEnvironment.systemContext + private val sharedPreferences = context.getSharedPreferences("tests", Context.MODE_PRIVATE) + private val storage = WCSessionStoreType(sharedPreferences) + + companion object { + const val SESSION_KEY = "org.walletconnect.session" + } + + @Before + fun before() { + sharedPreferences.edit().clear().commit() + } + + @Test + fun test_store() { + val topic = "topic_1" + val session = WCSession.from("wc:$topic@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=some_key")!! + val item = WCSessionStoreItem(session, 56, "peerId", "remotePeerId", WCPeerMeta(name = "Some DApp", url = "https://dapp.com")) + + storage.session = item + Assert.assertNotNull(sharedPreferences.getString(SESSION_KEY, null)) + } + + @Test + fun test_remove() { + val topic = "topic_1" + val session = WCSession.from("wc:$topic@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=some_key")!! + val item = WCSessionStoreItem(session, 56, "peerId","remotePeerId", WCPeerMeta(name = "Some DApp", url = "https://dapp.com")) + + storage.session = item + storage.session = null + Assert.assertFalse(sharedPreferences.contains(SESSION_KEY)) + } +} \ No newline at end of file diff --git a/walletconnect/src/test/java/com/trustwallet/walletconnect/models/WCSessionTests.kt b/walletconnect/src/test/java/com/trustwallet/walletconnect/models/WCSessionTests.kt new file mode 100644 index 0000000..0312f76 --- /dev/null +++ b/walletconnect/src/test/java/com/trustwallet/walletconnect/models/WCSessionTests.kt @@ -0,0 +1,26 @@ +package com.trustwallet.walletconnect.models + +import android.os.Build +import com.trustwallet.walletconnect.models.session.WCSession +import org.junit.Test + +import org.junit.Assert.* +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.O_MR1]) +class WCSessionTests { + + @Test + fun test_from() { + val uri = "wc:217374f6-8735-472d-a743-23bd7d26d106@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=d565a3e6cc792fa789bbea26b3f257fb436cfba2de48d2490b3e0248168d4b6b" + val session = WCSession.from(uri) + + assertEquals("217374f6-8735-472d-a743-23bd7d26d106", session?.topic) + assertEquals("1", session?.version) + assertEquals("https://bridge.walletconnect.org", session?.bridge) + assertEquals("d565a3e6cc792fa789bbea26b3f257fb436cfba2de48d2490b3e0248168d4b6b", session?.key) + } +} \ No newline at end of file diff --git a/walletconnect/src/test/java/com/trustwallet/walletconnect/models/binance/WCBinanceOrderTests.kt b/walletconnect/src/test/java/com/trustwallet/walletconnect/models/binance/WCBinanceOrderTests.kt new file mode 100644 index 0000000..4c12755 --- /dev/null +++ b/walletconnect/src/test/java/com/trustwallet/walletconnect/models/binance/WCBinanceOrderTests.kt @@ -0,0 +1,178 @@ +package com.trustwallet.walletconnect.models.binance + +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.registerTypeAdapter +import com.google.gson.GsonBuilder +import com.google.gson.JsonArray +import com.trustwallet.walletconnect.jsonrpc.JsonRpcRequest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test +import java.util.* + +class WCBinanceOrderTests { + val gson = GsonBuilder() + .registerTypeAdapter(cancelOrderDeserializer) + .registerTypeAdapter(cancelOrderSerializer) + .registerTypeAdapter(tradeOrderDeserializer) + .registerTypeAdapter(tradeOrderSerializer) + .registerTypeAdapter(transferOrderDeserializer) + .registerTypeAdapter(transferOrderSerializer) + .create() + + @Test + fun test_parseCancelOrder() { + val json = """ + { + "id": 1, + "jsonrpc": "2.0", + "method": "bnb_sign", + "params": [ + { + "account_number": "29", + "chain_id": "Binance-Chain-Tigris", + "data": null, + "memo": "", + "msgs": [ + { + "refid": "33BBF307B98146F13D20693CF946C2D77A4CAF28-300", + "sender": "bnb1xwalxpaes9r0z0fqdy70j3kz6aayetegur38gl", + "symbol": "PVT-554_BNB" + } + ], + "sequence": "300", + "source": "1" + } + ] + }""" + + + val request = gson.fromJson>(json) + val cancelOrder = gson.fromJson>(request.params).first() + assertNotNull(cancelOrder) + val cancelOrderJson = gson.toJson(cancelOrder) + assertEquals(cancelOrderJson, """{"account_number":"29","chain_id":"Binance-Chain-Tigris","memo":"","sequence":"300","source":"1","msgs":[{"refid":"33BBF307B98146F13D20693CF946C2D77A4CAF28-300","sender":"bnb1xwalxpaes9r0z0fqdy70j3kz6aayetegur38gl","symbol":"PVT-554_BNB"}]}""") + } + + @Test + fun test_parseTradeOrder() { + val json = """ + { + "id": 1, + "jsonrpc": "2.0", + "method": "bnb_sign", + "params": [ + { + "account_number": "29", + "chain_id": "Binance-Chain-Tigris", + "data": null, + "memo": "", + "msgs": [ + { + "id": "33BBF307B98146F13D20693CF946C2D77A4CAF28-300", + "ordertype": 2, + "price": 7800, + "quantity": 10000000000, + "sender": "bnb1xwalxpaes9r0z0fqdy70j3kz6aayetegur38gl", + "side": 1, + "symbol": "PVT-554_BNB", + "timeinforce": 1 + } + ], + "sequence": "299", + "source": "1" + } + ] + }""" + + + val request = gson.fromJson>(json) + val tradeOrder = gson.fromJson>(request.params).first() + assertNotNull(tradeOrder) + val cancelOrderJson = gson.toJson(tradeOrder) + assertEquals(cancelOrderJson, """{"account_number":"29","chain_id":"Binance-Chain-Tigris","memo":"","sequence":"299","source":"1","msgs":[{"id":"33BBF307B98146F13D20693CF946C2D77A4CAF28-300","ordertype":2,"price":7800,"quantity":10000000000,"sender":"bnb1xwalxpaes9r0z0fqdy70j3kz6aayetegur38gl","side":1,"symbol":"PVT-554_BNB","timeinforce":1}]}""") + } + + @Test + fun test_parseTransferOrder() { + val json = """ + { + "id": 1, + "jsonrpc": "2.0", + "method": "bnb_sign", + "params": [ + { + "account_number": "29", + "chain_id": "Binance-Chain-Tigris", + "data": null, + "memo": "Testing", + "msgs": [ + { + "inputs": [ + { + "address": "bnb1xwalxpaes9r0z0fqdy70j3kz6aayetegur38gl", + "coins": [ + { + "amount": 1000000, + "denom": "BNB" + } + ] + } + ], + "outputs": [ + { + "address": "bnb14u7newkxwdhcuhddvtg2n8n96m9tqxejsjuuhn", + "coins": [ + { + "amount": 1000000, + "denom": "BNB" + } + ] + } + ] + } + ], + "sequence": "301", + "source": "1" + } + ] + } + """ + + val request = gson.fromJson>(json) + val order = gson.fromJson>(request.params).first() + assertNotNull(order) + assertEquals(gson.toJson(order), """{"account_number":"29","chain_id":"Binance-Chain-Tigris","memo":"Testing","sequence":"301","source":"1","msgs":[{"inputs":[{"address":"bnb1xwalxpaes9r0z0fqdy70j3kz6aayetegur38gl","coins":[{"amount":1000000,"denom":"BNB"}]}],"outputs":[{"address":"bnb14u7newkxwdhcuhddvtg2n8n96m9tqxejsjuuhn","coins":[{"amount":1000000,"denom":"BNB"}]}]}]}""") + } + + @Test(expected = NoSuchElementException::class) + fun test_parseInvalidTradeOrder() { + val json = """ + { + "id": 1, + "jsonrpc": "2.0", + "method": "bnb_sign", + "params": [ + { + "account_number": "29", + "chain_id": "Binance-Chain-Tigris", + "data": null, + "memo": "", + "msgs": [ + { + "refid": "33BBF307B98146F13D20693CF946C2D77A4CAF28-300", + "sender": "bnb1xwalxpaes9r0z0fqdy70j3kz6aayetegur38gl", + "symbol": "PVT-554_BNB" + } + ], + "sequence": "300", + "source": "1" + } + ] + }""" + + + val request = gson.fromJson>(json) + gson.fromJson>(request.params).first() + } +} \ No newline at end of file diff --git a/walletconnect/src/test/java/com/trustwallet/walletconnect/models/binance/WCBinanceTradePairTests.kt b/walletconnect/src/test/java/com/trustwallet/walletconnect/models/binance/WCBinanceTradePairTests.kt new file mode 100644 index 0000000..758b4e8 --- /dev/null +++ b/walletconnect/src/test/java/com/trustwallet/walletconnect/models/binance/WCBinanceTradePairTests.kt @@ -0,0 +1,27 @@ +package com.trustwallet.walletconnect.models.binance + +import org.junit.Assert.* +import org.junit.Test + +class WCBinanceTradePairTests { + @Test + fun test_parse() { + val symbol = "BNB_ETH.B-261" + val pair = WCBinanceTradePair.from(symbol) + + assertEquals(pair?.from, "BNB") + assertEquals(pair?.to, "ETH.B") + + val symbol2 = "000-0E1_BNB" + val pair2 = WCBinanceTradePair.from(symbol2) + + assertEquals(pair2?.from, "000") + assertEquals(pair2?.to, "BNB") + + val symbol3 = "CRYPRICE-150_BTC.B-918" + val pair3 = WCBinanceTradePair.from(symbol3) + + assertEquals(pair3?.from, "CRYPRICE") + assertEquals(pair3?.to, "BTC.B") + } +} \ No newline at end of file diff --git a/walletconnect/src/test/java/com/trustwallet/walletconnect/models/ethereum/WCEthereumTransactionTests.kt b/walletconnect/src/test/java/com/trustwallet/walletconnect/models/ethereum/WCEthereumTransactionTests.kt new file mode 100644 index 0000000..18b23f1 --- /dev/null +++ b/walletconnect/src/test/java/com/trustwallet/walletconnect/models/ethereum/WCEthereumTransactionTests.kt @@ -0,0 +1,58 @@ +package com.trustwallet.walletconnect.models.ethereum + +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.registerTypeAdapter +import com.google.gson.GsonBuilder +import com.trustwallet.walletconnect.models.binance.ethTransactionSerializer +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class WCEthereumTransactionTests { + private val gson = GsonBuilder() + .registerTypeAdapter(ethTransactionSerializer) + .create() + + @Test + fun test_parseCancelOrder() { + val jsonString = """ + { + "from": "0xc36edf48e21cf395b206352a1819de658fd7f988", + "gas": "0x77fb", + "gasPrice": "0xb2d05e00", + "nonce": "0x64", + "to": "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e", + "value": "0x0", + "data": "" + } + """ + + + val tx = gson.fromJson(jsonString) + assertEquals(tx.gas, "0x77fb") + assertNull(tx.gasLimit) + } + + @Test + fun test_parseMultipleMessages() { + val jsonString = """ + [ + { + "from": "0xc36edf48e21cf395b206352a1819de658fd7f988", + "gas": "0x77fb", + "gasPrice": "0xb2d05e00", + "nonce": "0x64", + "to": "0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e", + "value": "0x0", + "data": "" + }, + "" + ] + """ + + val tx = gson.fromJson>(jsonString) + assertEquals(tx.size, 1) + assertEquals(tx.first()?.gas, "0x77fb") + assertNull(tx.first()?.gasLimit) + } +} \ No newline at end of file