diff --git a/README.md b/README.md index a11abe2d4..2377c6f3c 100644 --- a/README.md +++ b/README.md @@ -289,7 +289,7 @@ const { Client } = require('@xmtp/xmtp-react-native') async function main() { //Create a random wallet for example purposes. On the frontend you should replace it with the user's wallet (metamask, rainbow, etc) //Initialize the xmtp client - const xmtp = await XMTP.Client.createRandom("dev"); + const xmtp = await XMTP.Client.createRandom({ env: "dev" }); //In this example we are going to broadcast to the GM_BOT wallet (already activated) and a random wallet (not activated) const GM_BOT = '0x937C0d4a6294cdfa575de17382c7076b579DC176' @@ -362,7 +362,7 @@ import { Client } from '@xmtp/xmtp-react-native' // Get the keys using a valid Signer. Save them somewhere secure. const keys = await Client.exportKeyBundle() // Create a client using keys returned from getKeys -const client = await Client.createFromKeyBundle(keys, "dev") +const client = await Client.createFromKeyBundle(keys, { env: "dev" }) ``` The keys returned by `exportKeyBundle` should be treated with the utmost care as compromise of these keys will allow an attacker to impersonate the user on the XMTP network. Ensure these keys are stored somewhere secure and encrypted. diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 0b39c268b..c6d1c0a79 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -81,11 +81,27 @@ fun Conversation.cacheKey(clientAddress: String): String { } class XMTPModule : Module() { - private val apiEnvironments = mapOf( - "local" to ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false), - "dev" to ClientOptions.Api(env = XMTPEnvironment.DEV, isSecure = true), - "production" to ClientOptions.Api(env = XMTPEnvironment.PRODUCTION, isSecure = true) - ) + private fun apiEnvironments(env: String, appVersion: String?): ClientOptions.Api { + return when (env) { + "local" -> ClientOptions.Api( + env = XMTPEnvironment.LOCAL, + isSecure = false, + appVersion = appVersion + ) + + "production" -> ClientOptions.Api( + env = XMTPEnvironment.PRODUCTION, + isSecure = true, + appVersion = appVersion + ) + + else -> ClientOptions.Api( + env = XMTPEnvironment.DEV, + isSecure = true, + appVersion = appVersion + ) + } + } private var clients: MutableMap = mutableMapOf() private var xmtpPush: XMTPPush? = null @@ -107,12 +123,11 @@ class XMTPModule : Module() { // // Auth functions // - AsyncFunction("auth") { address: String, environment: String -> + AsyncFunction("auth") { address: String, environment: String, appVersion: String? -> logV("auth") val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address) signer = reactSigner - val options = - ClientOptions(api = apiEnvironments[environment] ?: apiEnvironments["dev"]!!) + val options = ClientOptions(api = apiEnvironments(environment, appVersion)) clients[address] = Client().create(account = reactSigner, options = options) signer = null sendEvent("authed") @@ -124,21 +139,19 @@ class XMTPModule : Module() { } // Generate a random wallet and set the client to that - AsyncFunction("createRandom") { environment: String -> + AsyncFunction("createRandom") { environment: String, appVersion: String? -> logV("createRandom") val privateKey = PrivateKeyBuilder() - val options = - ClientOptions(api = apiEnvironments[environment] ?: apiEnvironments["dev"]!!) + val options = ClientOptions(api = apiEnvironments(environment, appVersion)) val randomClient = Client().create(account = privateKey, options = options) clients[randomClient.address] = randomClient randomClient.address } - AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String -> + AsyncFunction("createFromKeyBundle") { keyBundle: String, environment: String, appVersion: String? -> try { logV("createFromKeyBundle") - val options = - ClientOptions(api = apiEnvironments[environment] ?: apiEnvironments["dev"]!!) + val options = ClientOptions(api = apiEnvironments(environment, appVersion)) val bundle = PrivateKeyOuterClass.PrivateKeyBundle.parseFrom( Base64.decode( diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 54cee0eea..0c063e281 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -409,16 +409,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.4.0-alpha0): + - XMTP (0.4.1-alpha0): - Connect-Swift - GzipSwift - web3.swift - - XMTPRust (= 0.3.0-beta0) + - XMTPRust (= 0.3.1-beta0) - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - - XMTP (= 0.4.0-alpha0) - - XMTPRust (0.3.0-beta0) + - XMTP (= 0.4.1-alpha0) + - XMTPRust (0.3.1-beta0) - Yoga (1.14.0) DEPENDENCIES: @@ -660,9 +660,9 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 40bd808372cb8706108f22d28f8ab4a6b9bc6989 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 8bb7ef01ba2ed0db8bbf699164d2da22dd989a25 - XMTPReactNative: c8f5e3f2d2f1e9102b9fd888047b34919be057d4 - XMTPRust: 233518ed46fbe3ea9e3bc3035de9a620dba09ce5 + XMTP: a5be4f9bcae69a821b258c76b7aaed2701961cac + XMTPReactNative: 74eb825a1acf17580a9e6bacb0aadb15724a4dd4 + XMTPRust: 78f65f77b1454392980da244961777aee955652f Yoga: ba09b6b11e6139e3df8229238aa794205ca6a02a PODFILE CHECKSUM: 522d88edc2d5fac4825e60a121c24abc18983367 diff --git a/example/src/AuthView.tsx b/example/src/AuthView.tsx index 645cc8f99..230254245 100644 --- a/example/src/AuthView.tsx +++ b/example/src/AuthView.tsx @@ -19,14 +19,20 @@ function AuthView({ useEffect(() => { (async () => { if (signer) { - const client = await XMTP.Client.create(signer, "dev"); + const client = await XMTP.Client.create(signer, { + env: "dev", + appVersion: "XMTP_RN_EX/0.0.1", + }); setClient(client); } })(); }, [signer]); const generateWallet = async () => { - const client = await XMTP.Client.createRandom("dev"); + const client = await XMTP.Client.createRandom({ + env: "dev", + appVersion: "XMTP_RN_EX/0.0.1", + }); setClient(client); }; diff --git a/example/src/tests.ts b/example/src/tests.ts index be86c7ca7..6f10d7044 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -1,15 +1,11 @@ import { content } from "@xmtp/proto"; +import { randomBytes } from "crypto"; import { NumberCodec, TextCodec } from "./test_utils"; import * as XMTP from "../../src/index"; import { DecodedMessage, Query } from "../../src/index"; import { CodecError } from "../../src/lib/CodecError"; import { CodecRegistry } from "../../src/lib/CodecRegistry"; -import { randomBytes } from "crypto"; - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} export type Test = { name: string; @@ -27,7 +23,10 @@ function test(name: string, perform: () => Promise) { // }); test("can make a client", async () => { - const client = await XMTP.Client.createRandom("local"); + const client = await XMTP.Client.createRandom({ + env: "local", + appVersion: "Testing/0.0.0", + }); return client.address.length > 0; }); @@ -44,8 +43,8 @@ test("can send and receive a text codec", async () => { const data = content.EncodedContent.encode(encodedContent).finish(); - const bob = await XMTP.Client.createRandom("local"); - const alice = await XMTP.Client.createRandom("local"); + const bob = await XMTP.Client.createRandom({ env: "local" }); + const alice = await XMTP.Client.createRandom({ env: "local" }); if (bob.address === alice.address) { throw new Error("bob and alice should be different"); @@ -89,8 +88,8 @@ test("can pass a custom filter date and receive message objects with expected da const data = content.EncodedContent.encode(encodedContent).finish(); - const bob = await XMTP.Client.createRandom("local"); - const alice = await XMTP.Client.createRandom("local"); + const bob = await XMTP.Client.createRandom({ env: "local" }); + const alice = await XMTP.Client.createRandom({ env: "local" }); if (bob.address === alice.address) { throw new Error("bob and alice should be different"); @@ -129,8 +128,8 @@ test("can pass a custom filter date and receive message objects with expected da }); test("canMessage", async () => { - const bob = await XMTP.Client.createRandom("local"); - const alice = await XMTP.Client.createRandom("local"); + const bob = await XMTP.Client.createRandom({ env: "local" }); + const alice = await XMTP.Client.createRandom({ env: "local" }); const canMessage = await bob.canMessage(alice.address); return canMessage; @@ -201,8 +200,8 @@ test("can send and receive number codec", async () => { const data = content.EncodedContent.encode(encodedContent).finish(); - const bob = await XMTP.Client.createRandom("local"); - const alice = await XMTP.Client.createRandom("local"); + const bob = await XMTP.Client.createRandom({ env: "local" }); + const alice = await XMTP.Client.createRandom({ env: "local" }); if (bob.address === alice.address) { throw new Error("bob and alice should be different"); @@ -235,7 +234,9 @@ test("can send and receive number codec", async () => { test("createFromKeyBundle throws error for non string value", async () => { try { const bytes = randomBytes(32); - await XMTP.Client.createFromKeyBundle(JSON.stringify(bytes), "local"); + await XMTP.Client.createFromKeyBundle(JSON.stringify(bytes), { + env: "local", + }); } catch (e) { return true; } @@ -255,8 +256,8 @@ test("can list batch messages", async () => { const data = content.EncodedContent.encode(encodedContent).finish(); - const bob = await XMTP.Client.createRandom("local"); - const alice = await XMTP.Client.createRandom("local"); + const bob = await XMTP.Client.createRandom({ env: "local" }); + const alice = await XMTP.Client.createRandom({ env: "local" }); if (bob.address === alice.address) { throw new Error("bob and alice should be different"); diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 9ee31a8d6..5ba0e718e 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -70,20 +70,28 @@ extension Conversation { } public class XMTPModule: Module { - var apiEnvironments = [ - "local": XMTP.ClientOptions.Api( - env: XMTP.XMTPEnvironment.local, - isSecure: false - ), - "dev": XMTP.ClientOptions.Api( - env: XMTP.XMTPEnvironment.dev, - isSecure: true - ), - "production": XMTP.ClientOptions.Api( - env: XMTP.XMTPEnvironment.production, - isSecure: true - ), - ] + private func apiEnvironments(env: String, appVersion: String?) -> XMTP.ClientOptions.Api { + switch env { + case "local": + return XMTP.ClientOptions.Api( + env: XMTP.XMTPEnvironment.local, + isSecure: false, + appVersion: appVersion + ) + case "production": + return XMTP.ClientOptions.Api( + env: XMTP.XMTPEnvironment.production, + isSecure: true, + appVersion: appVersion + ) + default: + return XMTP.ClientOptions.Api( + env: XMTP.XMTPEnvironment.dev, + isSecure: true, + appVersion: appVersion + ) + } + } var clients: [String: XMTP.Client] = [:] var signer: ReactNativeSigner? @@ -111,10 +119,10 @@ public class XMTPModule: Module { // // Auth functions // - AsyncFunction("auth") { (address: String, environment: String) in + AsyncFunction("auth") { (address: String, environment: String, appVersion: String?) in let signer = ReactNativeSigner(module: self, address: address) self.signer = signer - let options = XMTP.ClientOptions(api: apiEnvironments[environment] ?? apiEnvironments["local"]!) + let options = XMTP.ClientOptions(api: apiEnvironments(env: environment, appVersion: appVersion)) self.clients[address] = try await XMTP.Client.create(account: signer, options: options) self.signer = nil sendEvent("authed") @@ -125,9 +133,9 @@ public class XMTPModule: Module { } // Generate a random wallet and set the client to that - AsyncFunction("createRandom") { (environment: String) -> String in + AsyncFunction("createRandom") { (environment: String, appVersion: String?) -> String in let privateKey = try PrivateKey.generate() - let options = XMTP.ClientOptions(api: apiEnvironments[environment] ?? apiEnvironments["dev"]!) + let options = XMTP.ClientOptions(api: apiEnvironments(env: environment, appVersion: appVersion)) let client = try await Client.create(account: privateKey, options: options) self.clients[client.address] = client @@ -135,14 +143,14 @@ public class XMTPModule: Module { } // Create a client using its serialized key bundle. - AsyncFunction("createFromKeyBundle") { (keyBundle: String, environment: String) -> String in + AsyncFunction("createFromKeyBundle") { (keyBundle: String, environment: String, appVersion: String?) -> String in do { guard let keyBundleData = Data(base64Encoded: keyBundle), let bundle = try? PrivateKeyBundle(serializedData: keyBundleData) else { throw Error.invalidKeyBundle } - let options = XMTP.ClientOptions(api: apiEnvironments[environment] ?? apiEnvironments["dev"]!) + let options = XMTP.ClientOptions(api: apiEnvironments(env: environment, appVersion: appVersion)) let client = try await Client.from(bundle: bundle, options: options) self.clients[client.address] = client return client.address diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 385377475..afc7d4206 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -25,5 +25,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.4.0-alpha0" + s.dependency "XMTP", "= 0.4.1-alpha0" end diff --git a/src/index.ts b/src/index.ts index c88a9f4fd..25d01b7b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,9 +14,10 @@ export function address(): string { export async function auth( address: string, - environment: "local" | "dev" | "production" + environment: "local" | "dev" | "production", + appVersion?: string | undefined ) { - return await XMTPModule.auth(address, environment); + return await XMTPModule.auth(address, environment, appVersion); } export async function receiveSignature(requestID: string, signature: string) { @@ -24,16 +25,22 @@ export async function receiveSignature(requestID: string, signature: string) { } export async function createRandom( - environment: "local" | "dev" | "production" + environment: "local" | "dev" | "production", + appVersion?: string | undefined ): Promise { - return await XMTPModule.createRandom(environment); + return await XMTPModule.createRandom(environment, appVersion); } export async function createFromKeyBundle( keyBundle: string, - environment: "local" | "dev" | "production" + environment: "local" | "dev" | "production", + appVersion?: string | undefined ): Promise { - return await XMTPModule.createFromKeyBundle(keyBundle, environment); + return await XMTPModule.createFromKeyBundle( + keyBundle, + environment, + appVersion + ); } export async function exportKeyBundle(clientAddress: string): Promise { diff --git a/src/lib/Client.ts b/src/lib/Client.ts index 92e631274..a92c3beed 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -1,10 +1,10 @@ import { Signer, utils } from "ethers"; import Conversations from "./Conversations"; +import { DecodedMessage } from "./DecodedMessage"; import { Query } from "./Query"; import { hexToBytes } from "./util"; import * as XMTPModule from "../index"; -import { DecodedMessage } from "./DecodedMessage"; declare const Buffer; export class Client { @@ -13,8 +13,9 @@ export class Client { static async create( signer: Signer, - environment: "local" | "dev" | "production" + opts?: Partial ): Promise { + const options = defaultOptions(opts); return new Promise((resolve, reject) => { (async () => { XMTPModule.emitter.addListener( @@ -40,25 +41,33 @@ export class Client { const address = await signer.getAddress(); resolve(new Client(address)); }); - XMTPModule.auth(await signer.getAddress(), environment); + XMTPModule.auth( + await signer.getAddress(), + options.env, + options.appVersion + ); })(); }); } - static async createRandom( - environment: "local" | "dev" | "production" - ): Promise { - const address = await XMTPModule.createRandom(environment); + static async createRandom(opts?: Partial): Promise { + const options = defaultOptions(opts); + const address = await XMTPModule.createRandom( + options.env, + options.appVersion + ); return new Client(address); } static async createFromKeyBundle( keyBundle: string, - environment: "local" | "dev" | "production" + opts?: Partial ): Promise { + const options = defaultOptions(opts); const address = await XMTPModule.createFromKeyBundle( keyBundle, - environment + options.env, + options.appVersion ); return new Client(address); } @@ -89,3 +98,35 @@ export class Client { } } } + +export type ClientOptions = NetworkOptions; +export type NetworkOptions = { + /** + * Specify which XMTP environment to connect to. (default: `dev`) + */ + env: "local" | "dev" | "production"; + /** + * identifier that's included with API requests. + * + * For example, you can use the following format: + * `appVersion: APP_NAME + '/' + APP_VERSION`. + * Setting this value provides telemetry that shows which apps are + * using the XMTP client SDK. This information can help XMTP developers + * provide app support, especially around communicating important + * SDK updates, including deprecations and required upgrades. + */ + appVersion?: string; +}; + +/** + * Provide a default client configuration. These settings can be used on their own, or as a starting point for custom configurations + * + * @param opts additional options to override the default settings + */ +export function defaultOptions(opts?: Partial): ClientOptions { + const _defaultOptions: ClientOptions = { + env: "dev", + }; + + return { ..._defaultOptions, ...opts } as ClientOptions; +}