diff --git a/android/build.gradle b/android/build.gradle index d213de4ab..1403dc5f2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:3.0.6" + implementation "org.xmtp:android:3.0.8" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.facebook.react:react-native:0.71.3' implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index d2c608d38..0a9db884a 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -25,6 +25,7 @@ import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper import expo.modules.xmtpreactnativesdk.wrappers.InboxStateWrapper import expo.modules.xmtpreactnativesdk.wrappers.MemberWrapper import expo.modules.xmtpreactnativesdk.wrappers.PermissionPolicySetWrapper +import expo.modules.xmtpreactnativesdk.wrappers.WalletParamsWrapper import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -253,19 +254,6 @@ class XMTPModule : Module() { } } - AsyncFunction("revokeAllOtherInstallations") Coroutine { inboxId: String -> - withContext(Dispatchers.IO) { - logV("revokeAllOtherInstallations") - val client = clients[inboxId] ?: throw XMTPException("No client") - val reactSigner = - ReactNativeSigner(module = this@XMTPModule, address = client.address) - signer = reactSigner - - client.revokeAllOtherInstallations(reactSigner) - signer = null - } - } - AsyncFunction("getInboxState") Coroutine { inboxId: String, refreshFromNetwork: Boolean -> withContext(Dispatchers.IO) { val client = clients[inboxId] ?: throw XMTPException("No client") @@ -317,16 +305,16 @@ class XMTPModule : Module() { } } - AsyncFunction("create") Coroutine { address: String, hasAuthInboxCallback: Boolean?, dbEncryptionKey: List, authParams: String -> + AsyncFunction("create") Coroutine { address: String, hasAuthInboxCallback: Boolean?, dbEncryptionKey: List, authParams: String, walletParams: String -> withContext(Dispatchers.IO) { logV("create") - val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) + val walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) val reactSigner = ReactNativeSigner( module = this@XMTPModule, address = address, - type = authOptions.walletType, - chainId = authOptions.chainId, - blockNumber = authOptions.blockNumber + type = walletOptions.walletType, + chainId = walletOptions.chainId, + blockNumber = walletOptions.blockNumber ) signer = reactSigner val options = clientOptions( @@ -356,6 +344,66 @@ class XMTPModule : Module() { } } + AsyncFunction("revokeAllOtherInstallations") Coroutine { inboxId: String, walletParams: String -> + withContext(Dispatchers.IO) { + logV("revokeAllOtherInstallations") + val client = clients[inboxId] ?: throw XMTPException("No client") + val walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) + val reactSigner = + ReactNativeSigner( + module = this@XMTPModule, + address = client.address, + type = walletOptions.walletType, + chainId = walletOptions.chainId, + blockNumber = walletOptions.blockNumber + ) + signer = reactSigner + + client.revokeAllOtherInstallations(reactSigner) + signer = null + } + } + + AsyncFunction("addAccount") Coroutine { inboxId: String, newAddress: String, walletParams: String -> + withContext(Dispatchers.IO) { + logV("addAccount") + val client = clients[inboxId] ?: throw XMTPException("No client") + val walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) + val reactSigner = + ReactNativeSigner( + module = this@XMTPModule, + address = newAddress, + type = walletOptions.walletType, + chainId = walletOptions.chainId, + blockNumber = walletOptions.blockNumber + ) + signer = reactSigner + + client.addAccount(reactSigner) + signer = null + } + } + + AsyncFunction("removeAccount") Coroutine { inboxId: String, addressToRemove: String, walletParams: String -> + withContext(Dispatchers.IO) { + logV("removeAccount") + val client = clients[inboxId] ?: throw XMTPException("No client") + val walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) + val reactSigner = + ReactNativeSigner( + module = this@XMTPModule, + address = client.address, + type = walletOptions.walletType, + chainId = walletOptions.chainId, + blockNumber = walletOptions.blockNumber + ) + signer = reactSigner + + client.removeAccount(reactSigner, addressToRemove) + signer = null + } + } + AsyncFunction("dropClient") Coroutine { inboxId: String -> withContext(Dispatchers.IO) { logV("dropClient") @@ -366,6 +414,7 @@ class XMTPModule : Module() { AsyncFunction("signWithInstallationKey") Coroutine { inboxId: String, message: String -> withContext(Dispatchers.IO) { + logV("signWithInstallationKey") val client = clients[inboxId] ?: throw XMTPException("No client") val signature = client.signWithInstallationKey(message) @@ -373,6 +422,18 @@ class XMTPModule : Module() { } } + AsyncFunction("verifySignature") Coroutine { inboxId: String, message: String, signature: List -> + withContext(Dispatchers.IO) { + logV("verifySignature") + val client = clients[inboxId] ?: throw XMTPException("No client") + val signatureBytes = + signature.foldIndexed(ByteArray(signature.size)) { i, a, v -> + a.apply { set(i, v.toByte()) } + } + client.verifySignature(message, signatureBytes) + } + } + AsyncFunction("canMessage") Coroutine { inboxId: String, peerAddresses: List -> withContext(Dispatchers.IO) { logV("canMessage") diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt index feeae6b21..1273912f3 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt @@ -8,9 +8,6 @@ class AuthParamsWrapper( val appVersion: String?, val dbDirectory: String?, val historySyncUrl: String?, - val walletType: WalletType = WalletType.EOA, - val chainId: Long?, - val blockNumber: Long?, ) { companion object { fun authParamsFromJson(authParams: String): AuthParamsWrapper { @@ -20,6 +17,21 @@ class AuthParamsWrapper( if (jsonOptions.has("appVersion")) jsonOptions.get("appVersion").asString else null, if (jsonOptions.has("dbDirectory")) jsonOptions.get("dbDirectory").asString else null, if (jsonOptions.has("historySyncUrl")) jsonOptions.get("historySyncUrl").asString else null, + ) + } + } +} + +class WalletParamsWrapper( + val walletType: WalletType = WalletType.EOA, + val chainId: Long?, + val blockNumber: Long?, + + ) { + companion object { + fun walletParamsFromJson(walletParams: String): WalletParamsWrapper { + val jsonOptions = JsonParser.parseString(walletParams).asJsonObject + return WalletParamsWrapper( if (jsonOptions.has("walletType")) { when (jsonOptions.get("walletType").asString) { "SCW" -> WalletType.SCW @@ -30,7 +42,8 @@ class AuthParamsWrapper( }, if (jsonOptions.has("chainId")) jsonOptions.get("chainId").asLong else null, if (jsonOptions.has("blockNumber")) jsonOptions.get("blockNumber").asLong else null, - ) + ) } } } + diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index e7d7a21b1..2cdbd55fb 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,5 +1,4 @@ PODS: - - BigInt (5.0.0) - boost (1.76.0) - CoinbaseWalletSDK/Client (1.0.4) - CoinbaseWalletSDK/CrossPlatform (1.0.4): @@ -7,8 +6,10 @@ PODS: - CoinbaseWalletSDKExpo (1.0.10): - CoinbaseWalletSDK/CrossPlatform (= 1.0.4) - ExpoModulesCore - - Connect-Swift (0.12.0): - - SwiftProtobuf (~> 1.25.2) + - Connect-Swift (1.0.0): + - SwiftProtobuf (~> 1.28.2) + - CryptoSwift (1.8.3) + - CSecp256k1 (0.2.0) - DoubleConversion (1.1.6) - EXApplication (5.4.0): - ExpoModulesCore @@ -49,15 +50,12 @@ PODS: - React-jsi (= 0.71.14) - ReactCommon/turbomodule/core (= 0.71.14) - fmt (6.2.1) - - GenericJSON (2.0.2) - glog (0.3.5) - - GzipSwift (5.1.1) - hermes-engine (0.71.14): - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (3.0.3) - - Logging (1.0.0) + - LibXMTP (3.0.7) - MessagePacker (0.4.7) - MMKV (2.0.0): - MMKVCore (~> 2.0.0) @@ -442,23 +440,17 @@ PODS: - React-RCTImage - RNSVG (13.14.0): - React-Core - - secp256k1.swift (0.1.4) - - SwiftProtobuf (1.25.2) - - web3.swift (1.6.0): - - BigInt (~> 5.0.0) - - GenericJSON (~> 2.0) - - Logging (~> 1.0.0) - - secp256k1.swift (~> 0.1) - - XMTP (3.0.6): - - Connect-Swift (= 0.12.0) - - GzipSwift - - LibXMTP (= 3.0.3) - - web3.swift + - SwiftProtobuf (1.28.2) + - XMTP (3.0.8): + - Connect-Swift (= 1.0.0) + - CryptoSwift (= 1.8.3) + - CSecp256k1 (~> 0.2) + - LibXMTP (= 3.0.7) - XMTPReactNative (0.1.0): + - CSecp256k1 (~> 0.2) - ExpoModulesCore - MessagePacker - - secp256k1.swift - - XMTP (= 3.0.6) + - XMTP (= 3.0.8) - Yoga (1.14.0) DEPENDENCIES: @@ -533,22 +525,18 @@ DEPENDENCIES: SPEC REPOS: trunk: - - BigInt - CoinbaseWalletSDK - Connect-Swift + - CryptoSwift + - CSecp256k1 - fmt - - GenericJSON - - GzipSwift - libevent - LibXMTP - - Logging - MessagePacker - MMKV - MMKVCore - OpenSSL-Universal - - secp256k1.swift - SwiftProtobuf - - web3.swift - XMTP EXTERNAL SOURCES: @@ -684,11 +672,12 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - BigInt: 74b4d88367b0e819d9f77393549226d36faeb0d8 boost: 57d2868c099736d80fcd648bf211b4431e51a558 CoinbaseWalletSDK: ea1f37512bbc69ebe07416e3b29bf840f5cc3152 CoinbaseWalletSDKExpo: c79420eb009f482f768c23b6768fc5b2d7c98777 - Connect-Swift: 1de2ef4a548c59ecaeb9120812dfe0d6e07a0d47 + Connect-Swift: 84e043b904f63dc93a2c01c6c125da25e765b50d + CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 + CSecp256k1: 2a59c03e52637ded98896a33be4b2649392cb843 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 EXApplication: 2a0d9abd4feace9c6faedfe541c1dec02e3702cd EXConstants: f348da07e21b23d2b085e270d7b74f282df1a7d9 @@ -706,13 +695,10 @@ SPEC CHECKSUMS: FBLazyVector: 12ea01e587c9594e7b144e1bfc86ac4d9ac28fde FBReactNativeSpec: b6ae48e67aaba46442f84d6f9ba598ccfbe2ee66 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - GenericJSON: 79a840eeb77030962e8cf02a62d36bd413b67626 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: 948d39cf5b978adaa7d0f6ea5c6c0995a0b9e63f - Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 + LibXMTP: 1d9b9514c2b6407a82d72b203288cd5af064d787 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390 @@ -760,11 +746,9 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 - secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 - SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 - web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 48d0c71ef732ac4d79c2942902a132bf71661029 - XMTPReactNative: 406f92e777c9d2891404956309d926e7b2f25c1c + SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d + XMTP: f8ee66c4aacc5750ab0f67f5bb86c05cd2513185 + XMTPReactNative: 8cda2ca09be81cee8c26d986db65fc6faef57bea Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/example/ios/xmtpreactnativesdkexample.xcodeproj/project.pbxproj b/example/ios/xmtpreactnativesdkexample.xcodeproj/project.pbxproj index 0d2d68566..c977d86d6 100644 --- a/example/ios/xmtpreactnativesdkexample.xcodeproj/project.pbxproj +++ b/example/ios/xmtpreactnativesdkexample.xcodeproj/project.pbxproj @@ -293,13 +293,17 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-xmtpreactnativesdkexample/Pods-xmtpreactnativesdkexample-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift/CryptoSwift.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SwiftProtobuf/SwiftProtobuf.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/CryptoSwift.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SwiftProtobuf.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/example/src/tests/clientTests.ts b/example/src/tests/clientTests.ts index dba0cbb74..0425c3ebd 100644 --- a/example/src/tests/clientTests.ts +++ b/example/src/tests/clientTests.ts @@ -34,18 +34,6 @@ test('can make a client', async () => { return true }) -test('can sign with installation key', async () => { - const [client] = await createClients(1) - - const signature = await client.signWithInstallationKey('A digest message') - - assert( - signature !== undefined && signature.length > 0, - `Signature should not be empty but was: ${signature}` - ) - return true -}) - test('can revoke all other installations', async () => { const keyBytes = new Uint8Array([ 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, @@ -309,3 +297,68 @@ test('can find others inbox states', async () => { return true }) + +test('can verify signatures', async () => { + const [alix, bo] = await createClients(2) + const signature = await alix.signWithInstallationKey('a message') + + assert( + (await alix.verifySignature('a message', signature)) === true, + `message should verify` + ) + + assert( + (await alix.verifySignature('bad string', signature)) === false, + `message should not verify for bad string` + ) + + assert( + (await bo.verifySignature('a message', signature)) === false, + `message should not verify for bo` + ) + + return true +}) + +test('can add and remove accounts', async () => { + const keyBytes = new Uint8Array([ + 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, + 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, + ]) + + const alixWallet = Wallet.createRandom() + const alixWallet2 = Wallet.createRandom() + const alixWallet3 = Wallet.createRandom() + + const alix = await Client.create(alixWallet, { + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + }) + + await alix.addAccount(alixWallet2) + await alix.addAccount(alixWallet3) + + const inboxState = await alix.inboxState(true) + assert( + inboxState.addresses.length === 3, + `addresses length should be 3 but was ${inboxState.addresses.length}` + ) + assert( + inboxState.installations.length === 1, + `addresses length should be 1 but was ${inboxState.installations.length}` + ) + assert( + inboxState.recoveryAddress === alix.address.toLowerCase(), + `recovery address should be ${alix.address} but was ${inboxState.recoveryAddress}` + ) + + await alix.removeAccount(alixWallet, await alixWallet3.getAddress()) + const inboxState2 = await alix.inboxState(true) + assert( + inboxState2.addresses.length === 2, + `addresses length should be 2 but was ${inboxState.addresses.length}` + ) + + return true +}) diff --git a/ios/Wrappers/AuthParamsWrapper.swift b/ios/Wrappers/AuthParamsWrapper.swift index 4a9ccfd1e..04d1f517d 100644 --- a/ios/Wrappers/AuthParamsWrapper.swift +++ b/ios/Wrappers/AuthParamsWrapper.swift @@ -13,48 +13,77 @@ struct AuthParamsWrapper { let appVersion: String? let dbDirectory: String? let historySyncUrl: String? - let walletType: WalletType - let chainId: Int64? - let blockNumber: Int64? - - init(environment: String, appVersion: String?, dbDirectory: String?, historySyncUrl: String?, walletType: WalletType, chainId: Int64?, blockNumber: Int64?) { + + init( + environment: String, appVersion: String?, dbDirectory: String?, + historySyncUrl: String? + ) { self.environment = environment self.appVersion = appVersion self.dbDirectory = dbDirectory self.historySyncUrl = historySyncUrl - self.walletType = walletType - self.chainId = chainId - self.blockNumber = blockNumber } static func authParamsFromJson(_ authParams: String) -> AuthParamsWrapper { guard let data = authParams.data(using: .utf8), - let jsonOptions = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - return AuthParamsWrapper(environment: "dev", appVersion: nil, dbDirectory: nil, historySyncUrl: nil, walletType: WalletType.EOA, chainId: nil, blockNumber: nil) + let jsonOptions = try? JSONSerialization.jsonObject( + with: data, options: []) as? [String: Any] + else { + return AuthParamsWrapper( + environment: "dev", appVersion: nil, dbDirectory: nil, + historySyncUrl: nil) } let environment = jsonOptions["environment"] as? String ?? "dev" let appVersion = jsonOptions["appVersion"] as? String let dbDirectory = jsonOptions["dbDirectory"] as? String let historySyncUrl = jsonOptions["historySyncUrl"] as? String + + return AuthParamsWrapper( + environment: environment, + appVersion: appVersion, + dbDirectory: dbDirectory, + historySyncUrl: historySyncUrl + ) + } +} + +struct WalletParamsWrapper { + let walletType: WalletType + let chainId: Int64? + let blockNumber: Int64? + + init(walletType: WalletType, chainId: Int64?, blockNumber: Int64?) { + self.walletType = walletType + self.chainId = chainId + self.blockNumber = blockNumber + } + + static func walletParamsFromJson(_ walletParams: String) + -> WalletParamsWrapper + { + guard let data = walletParams.data(using: .utf8), + let jsonOptions = try? JSONSerialization.jsonObject( + with: data, options: []) as? [String: Any] + else { + return WalletParamsWrapper( + walletType: WalletType.EOA, chainId: nil, blockNumber: nil) + } + let walletTypeString = jsonOptions["walletType"] as? String ?? "EOA" let chainId = jsonOptions["chainId"] as? Int64 let blockNumber = jsonOptions["blockNumber"] as? Int64 - - let walletType = { switch walletTypeString { + + let walletType = { + switch walletTypeString { case "SCW": - return WalletType.SCW + return WalletType.SCW default: return WalletType.EOA } }() - - return AuthParamsWrapper( - environment: environment, - appVersion: appVersion, - dbDirectory: dbDirectory, - historySyncUrl: historySyncUrl, + return WalletParamsWrapper( walletType: walletType, chainId: chainId, blockNumber: blockNumber diff --git a/ios/Wrappers/DecodedMessageWrapper.swift b/ios/Wrappers/DecodedMessageWrapper.swift index 89b611009..21f3bf535 100644 --- a/ios/Wrappers/DecodedMessageWrapper.swift +++ b/ios/Wrappers/DecodedMessageWrapper.swift @@ -220,15 +220,10 @@ struct EncryptedAttachmentMetadata { } static func fromJsonObj(_ obj: [String: Any]) throws -> EncryptedAttachmentMetadata { - guard let secret = (obj["secret"] as? String ?? "").web3.hexData else { - throw Error.badRemoteAttachmentMetadata - } - guard let salt = (obj["salt"] as? String ?? "").web3.hexData else { - throw Error.badRemoteAttachmentMetadata - } - guard let nonce = (obj["nonce"] as? String ?? "").web3.hexData else { - throw Error.badRemoteAttachmentMetadata - } + let secret = (obj["secret"] as? String ?? "").hexToData + let salt = (obj["salt"] as? String ?? "").hexToData + let nonce = (obj["nonce"] as? String ?? "").hexToData + return EncryptedAttachmentMetadata( filename: obj["filename"] as? String ?? "", secret: secret, diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 1aca4bc6d..06c39289d 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -149,19 +149,6 @@ public class XMTPModule: Module { try await client.requestMessageHistorySync() } - AsyncFunction("revokeAllOtherInstallations") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) - else { - throw Error.noClient - } - let signer = ReactNativeSigner( - module: self, address: client.address) - self.signer = signer - - try await client.revokeAllOtherInstallations(signingKey: signer) - self.signer = nil - } - AsyncFunction("getInboxState") { (inboxId: String, refreshFromNetwork: Bool) -> String in guard let client = await clientsManager.getClient(key: inboxId) @@ -219,15 +206,11 @@ public class XMTPModule: Module { hasAuthenticateToInboxCallback ?? false ? self.preAuthenticateToInboxCallback : nil let encryptionKeyData = Data(dbEncryptionKey) - let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - let options = createClientConfig( - env: authOptions.environment, - appVersion: authOptions.appVersion, - preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, + let options = self.createClientConfig( + authParams: authParams, dbEncryptionKey: encryptionKeyData, - dbDirectory: authOptions.dbDirectory, - historySyncUrl: authOptions.historySyncUrl + preAuthenticateToInboxCallback: preAuthenticateToInboxCallback ) let client = try await Client.create( account: privateKey, options: options) @@ -240,14 +223,14 @@ public class XMTPModule: Module { AsyncFunction("create") { ( address: String, hasAuthenticateToInboxCallback: Bool?, - dbEncryptionKey: [UInt8], authParams: String + dbEncryptionKey: [UInt8], authParams: String, walletParams: String ) in - let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) + let walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) let signer = ReactNativeSigner( module: self, address: address, - walletType: authOptions.walletType, - chainId: authOptions.chainId, - blockNumber: authOptions.blockNumber) + walletType: walletOptions.walletType, + chainId: walletOptions.chainId, + blockNumber: walletOptions.blockNumber) self.signer = signer if hasAuthenticateToInboxCallback ?? false { self.preAuthenticateToInboxCallbackDeferred = DispatchSemaphore( @@ -259,12 +242,9 @@ public class XMTPModule: Module { let encryptionKeyData = Data(dbEncryptionKey) let options = self.createClientConfig( - env: authOptions.environment, - appVersion: authOptions.appVersion, - preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, + authParams: authParams, dbEncryptionKey: encryptionKeyData, - dbDirectory: authOptions.dbDirectory, - historySyncUrl: authOptions.historySyncUrl + preAuthenticateToInboxCallback: preAuthenticateToInboxCallback ) let client = try await XMTP.Client.create( account: signer, options: options) @@ -281,12 +261,9 @@ public class XMTPModule: Module { let encryptionKeyData = Data(dbEncryptionKey) let options = self.createClientConfig( - env: authOptions.environment, - appVersion: authOptions.appVersion, - preAuthenticateToInboxCallback: nil, + authParams: authParams, dbEncryptionKey: encryptionKeyData, - dbDirectory: authOptions.dbDirectory, - historySyncUrl: authOptions.historySyncUrl + preAuthenticateToInboxCallback: preAuthenticateToInboxCallback ) let client = try await XMTP.Client.build( address: address, options: options) @@ -294,6 +271,57 @@ public class XMTPModule: Module { key: client.inboxID, client: client) return try ClientWrapper.encodeToObj(client) } + + AsyncFunction("revokeAllOtherInstallations") { (inboxId: String, walletParams: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { + throw Error.noClient + } + let walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) + let signer = ReactNativeSigner( + module: self, address: client.address, + walletType: walletOptions.walletType, + chainId: walletOptions.chainId, + blockNumber: walletOptions.blockNumber) + self.signer = signer + + try await client.revokeAllOtherInstallations(signingKey: signer) + self.signer = nil + } + + AsyncFunction("addAccount") { (inboxId: String, newAddress: String, walletParams: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { + throw Error.noClient + } + let walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) + let signer = ReactNativeSigner( + module: self, address: newAddress, + walletType: walletOptions.walletType, + chainId: walletOptions.chainId, + blockNumber: walletOptions.blockNumber) + self.signer = signer + + try await client.addAccount(newAccount: signer) + self.signer = nil + } + + AsyncFunction("removeAccount") { (inboxId: String, addressToRemove: String, walletParams: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { + throw Error.noClient + } + let walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) + let signer = ReactNativeSigner( + module: self, address: client.address, + walletType: walletOptions.walletType, + chainId: walletOptions.chainId, + blockNumber: walletOptions.blockNumber) + self.signer = signer + + try await client.removeAccount(recoveryAccount: signer, addressToRemove: addressToRemove) + self.signer = nil + } // Remove a client from memory for a given inboxId AsyncFunction("dropClient") { (inboxId: String) in @@ -309,6 +337,15 @@ public class XMTPModule: Module { let signature = try client.signWithInstallationKey(message: message) return [UInt8](signature) } + + AsyncFunction("verifySignature") { + (inboxId: String, message: String, signature: [UInt8]) -> Bool in + guard let client = await clientsManager.getClient(key: inboxId) + else { + throw Error.noClient + } + return try client.verifySignature(message: message, signature: Data(signature)) + } AsyncFunction("canMessage") { (inboxId: String, peerAddresses: [String]) -> [String: Bool] in @@ -1663,17 +1700,16 @@ public class XMTPModule: Module { } func createClientConfig( - env: String, appVersion: String?, - preAuthenticateToInboxCallback: PreEventCallback? = nil, - dbEncryptionKey: Data, dbDirectory: String? = nil, - historySyncUrl: String? = nil + authParams: String, dbEncryptionKey: Data, + preAuthenticateToInboxCallback: PreEventCallback? = nil ) -> XMTP.ClientOptions { + let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) return XMTP.ClientOptions( - api: createApiClient(env: env, appVersion: appVersion), + api: createApiClient(env: authOptions.environment, appVersion: authOptions.appVersion), preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, - dbEncryptionKey: dbEncryptionKey, dbDirectory: dbDirectory, - historySyncUrl: historySyncUrl) + dbEncryptionKey: dbEncryptionKey, dbDirectory: authOptions.dbDirectory, + historySyncUrl: authOptions.historySyncUrl) } func subscribeToConversations(inboxId: String, type: ConversationType) diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 511f947e1..1be2289e8 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -24,7 +24,8 @@ Pod::Spec.new do |s| } s.source_files = "**/*.{h,m,swift}" - s.dependency 'secp256k1.swift' + s.dependency "MessagePacker" - s.dependency "XMTP", "= 3.0.6" + s.dependency "XMTP", "= 3.0.8" + s.dependency 'CSecp256k1', '~> 0.2' end diff --git a/src/index.ts b/src/index.ts index 59815a0bc..5fce15be0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -73,10 +73,6 @@ export async function requestMessageHistorySync(inboxId: InboxId) { return XMTPModule.requestMessageHistorySync(inboxId) } -export async function revokeAllOtherInstallations(inboxId: InboxId) { - return XMTPModule.revokeAllOtherInstallations(inboxId) -} - export async function getInboxState( inboxId: InboxId, refreshFromNetwork: boolean @@ -153,6 +149,8 @@ export async function create( appVersion, dbDirectory, historySyncUrl, + } + const walletParams: WalletParams = { walletType, chainId: typeof chainId === 'number' ? chainId : undefined, blockNumber: typeof blockNumber === 'number' ? blockNumber : undefined, @@ -161,7 +159,8 @@ export async function create( address, hasPreAuthenticateToInboxCallback, Array.from(dbEncryptionKey), - JSON.stringify(authParams) + JSON.stringify(authParams), + JSON.stringify(walletParams) ) } @@ -186,6 +185,61 @@ export async function build( ) } +export async function revokeAllOtherInstallations( + inboxId: InboxId, + walletType?: WalletType | undefined, + chainId?: number | undefined, + blockNumber?: number | undefined +) { + const walletParams: WalletParams = { + walletType, + chainId: typeof chainId === 'number' ? chainId : undefined, + blockNumber: typeof blockNumber === 'number' ? blockNumber : undefined, + } + return XMTPModule.revokeAllOtherInstallations( + inboxId, + JSON.stringify(walletParams) + ) +} + +export async function addAccount( + inboxId: InboxId, + newAddress: Address, + walletType?: WalletType | undefined, + chainId?: number | undefined, + blockNumber?: number | undefined +) { + const walletParams: WalletParams = { + walletType, + chainId: typeof chainId === 'number' ? chainId : undefined, + blockNumber: typeof blockNumber === 'number' ? blockNumber : undefined, + } + return XMTPModule.addAccount( + inboxId, + newAddress, + JSON.stringify(walletParams) + ) +} + +export async function removeAccount( + inboxId: InboxId, + addressToRemove: Address, + walletType?: WalletType | undefined, + chainId?: number | undefined, + blockNumber?: number | undefined +) { + const walletParams: WalletParams = { + walletType, + chainId: typeof chainId === 'number' ? chainId : undefined, + blockNumber: typeof blockNumber === 'number' ? blockNumber : undefined, + } + return XMTPModule.removeAccount( + inboxId, + addressToRemove, + JSON.stringify(walletParams) + ) +} + export async function dropClient(inboxId: InboxId) { return await XMTPModule.dropClient(inboxId) } @@ -201,6 +255,18 @@ export async function signWithInstallationKey( return new Uint8Array(signatureArray) } +export async function verifySignature( + inboxId: InboxId, + message: string, + signature: Uint8Array +): Promise { + return await XMTPModule.verifySignature( + inboxId, + message, + Array.from(signature) + ) +} + export async function canMessage( inboxId: InboxId, peerAddresses: Address[] @@ -990,6 +1056,9 @@ interface AuthParams { appVersion?: string dbDirectory?: string historySyncUrl?: string +} + +interface WalletParams { walletType?: string chainId?: number blockNumber?: number diff --git a/src/lib/Client.ts b/src/lib/Client.ts index 69a267dcb..e7f5abb9b 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -318,52 +318,96 @@ export class Client< this.codecRegistry[id] = contentCodec } - async signWithInstallationKey(message: string): Promise { - return XMTPModule.signWithInstallationKey(this.inboxId, message) - } - /** - * Find the Address associated with this address - * - * @param {string} peerAddress - The address of the peer to check for inboxId. - * @returns {Promise} A Promise resolving to the InboxId. + * Add this account to the current inboxId. + * @param {Signer} newAccount - The signer of the new account to be added. */ - async findInboxIdFromAddress( - peerAddress: Address - ): Promise { - return await XMTPModule.findInboxIdFromAddress(this.inboxId, peerAddress) - } + async addAccount(newAccount: Signer | WalletClient) { + const signer = getSigner(newAccount) + if (!signer) { + throw new Error('Signer is not configured') + } - /** - * Deletes the local database. This cannot be undone and these stored messages will not be refetched from the network. - */ - async deleteLocalDatabase() { - return await XMTPModule.deleteLocalDatabase(this.inboxId) - } + return new Promise((resolve, reject) => { + ;(async () => { + Client.signSubscription = XMTPModule.emitter.addListener( + 'sign', + async (message: { id: string; message: string }) => { + try { + await Client.handleSignatureRequest(signer, message) + } catch (e) { + const errorMessage = + 'ERROR in addAccount. User rejected signature' + console.info(errorMessage, e) + Client.signSubscription?.remove() + reject(errorMessage) + } + } + ) - /** - * Drop the local database connection. This function is delicate and should be used with caution. App will error if database not properly reconnected. See: reconnectLocalDatabase() - */ - async dropLocalDatabaseConnection() { - return await XMTPModule.dropLocalDatabaseConnection(this.inboxId) + await XMTPModule.addAccount( + this.inboxId, + await signer.getAddress(), + signer.walletType?.(), + signer.getChainId?.(), + signer.getBlockNumber?.() + ) + Client.signSubscription?.remove() + resolve() + })().catch((error) => { + Client.signSubscription?.remove() + reject(error) + }) + }) } /** - * Reconnects the local database after being dropped. + * Remove this account from the current inboxId. + * @param {Signer} wallet - The signer object used for authenticate the removal. + * @param {Address} addressToRemove - The address of the wallet you'd like to remove from the account. */ - async reconnectLocalDatabase() { - return await XMTPModule.reconnectLocalDatabase(this.inboxId) - } + async removeAccount(wallet: Signer | WalletClient, addressToRemove: Address) { + const signer = getSigner(wallet) + if (!signer) { + throw new Error('Signer is not configured') + } - /** - * Make a request for a message history sync. - */ - async requestMessageHistorySync() { - return await XMTPModule.requestMessageHistorySync(this.inboxId) + return new Promise((resolve, reject) => { + ;(async () => { + Client.signSubscription = XMTPModule.emitter.addListener( + 'sign', + async (message: { id: string; message: string }) => { + try { + await Client.handleSignatureRequest(signer, message) + } catch (e) { + const errorMessage = + 'ERROR in revokeAllOtherInstallations. User rejected signature' + console.info(errorMessage, e) + Client.signSubscription?.remove() + reject(errorMessage) + } + } + ) + + await XMTPModule.removeAccount( + this.inboxId, + addressToRemove, + signer.walletType?.(), + signer.getChainId?.(), + signer.getBlockNumber?.() + ) + Client.signSubscription?.remove() + resolve() + })().catch((error) => { + Client.signSubscription?.remove() + reject(error) + }) + }) } /** * Revoke all other installations but the current one. + * @param {Signer} signer - The signer object used for authenticate the revoke. */ async revokeAllOtherInstallations(wallet: Signer | WalletClient | null) { const signer = getSigner(wallet) @@ -388,7 +432,12 @@ export class Client< } ) - await XMTPModule.revokeAllOtherInstallations(this.inboxId) + await XMTPModule.revokeAllOtherInstallations( + this.inboxId, + signer.walletType?.(), + signer.getChainId?.(), + signer.getBlockNumber?.() + ) Client.signSubscription?.remove() resolve() })().catch((error) => { @@ -398,6 +447,68 @@ export class Client< }) } + /** + * Sign this message with the current installation key. + * @param {string} message - The message to sign. + * @returns {Promise} A Promise resolving to the signature bytes. + */ + async signWithInstallationKey(message: string): Promise { + return await XMTPModule.signWithInstallationKey(this.inboxId, message) + } + + /** + * Verify the signature was signed with this clients installation key. + * @param {string} message - The message that was signed. + * @param {Uint8Array} signature - The signature. + * @returns {Promise} A Promise resolving to a boolean if the signature verified or not. + */ + async verifySignature( + message: string, + signature: Uint8Array + ): Promise { + return await XMTPModule.verifySignature(this.inboxId, message, signature) + } + + /** + * Find the Address associated with this address + * + * @param {string} peerAddress - The address of the peer to check for inboxId. + * @returns {Promise} A Promise resolving to the InboxId. + */ + async findInboxIdFromAddress( + peerAddress: Address + ): Promise { + return await XMTPModule.findInboxIdFromAddress(this.inboxId, peerAddress) + } + + /** + * Deletes the local database. This cannot be undone and these stored messages will not be refetched from the network. + */ + async deleteLocalDatabase() { + return await XMTPModule.deleteLocalDatabase(this.inboxId) + } + + /** + * Drop the local database connection. This function is delicate and should be used with caution. App will error if database not properly reconnected. See: reconnectLocalDatabase() + */ + async dropLocalDatabaseConnection() { + return await XMTPModule.dropLocalDatabaseConnection(this.inboxId) + } + + /** + * Reconnects the local database after being dropped. + */ + async reconnectLocalDatabase() { + return await XMTPModule.reconnectLocalDatabase(this.inboxId) + } + + /** + * Make a request for a message history sync. + */ + async requestMessageHistorySync() { + return await XMTPModule.requestMessageHistorySync(this.inboxId) + } + /** * Make a request for your inbox state. * diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 1e74f5dc7..7eff0a365 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -281,7 +281,7 @@ export class Group< /** * Returns the group image url square. * To get the latest group image url square from the network, call sync() first. - * @returns {string} A Promise that resolves to the group name. + * @returns {string} A Promise that resolves to the group image url. */ async groupImageUrlSquare(): Promise { return XMTP.groupImageUrlSquare(this.client.inboxId, this.id)