Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create a pure v3 client #298

Merged
merged 14 commits into from
Sep 18, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class ClientTest {
fun testHasPrivateKeyBundleV1() {
val fakeWallet = PrivateKeyBuilder()
val client = runBlocking { Client().create(account = fakeWallet) }
assertEquals(1, client.privateKeyBundleV1.preKeysList?.size)
val preKey = client.privateKeyBundleV1.preKeysList?.get(0)
assertEquals(1, client.v1keys.preKeysList?.size)
val preKey = client.v1keys.preKeysList?.get(0)
assert(preKey?.publicKey?.hasSignature() ?: false)
}

Expand All @@ -56,12 +56,12 @@ class ClientTest {
val clientFromV1Bundle = runBlocking { Client().buildFromBundle(bundle) }
assertEquals(client.address, clientFromV1Bundle.address)
assertEquals(
client.privateKeyBundleV1.identityKey,
clientFromV1Bundle.privateKeyBundleV1.identityKey,
client.v1keys.identityKey,
clientFromV1Bundle.v1keys.identityKey,
)
assertEquals(
client.privateKeyBundleV1.preKeysList,
clientFromV1Bundle.privateKeyBundleV1.preKeysList,
client.v1keys.preKeysList,
clientFromV1Bundle.v1keys.preKeysList,
)
}

Expand All @@ -73,12 +73,12 @@ class ClientTest {
val clientFromV1Bundle = runBlocking { Client().buildFromV1Bundle(bundleV1) }
assertEquals(client.address, clientFromV1Bundle.address)
assertEquals(
client.privateKeyBundleV1.identityKey,
clientFromV1Bundle.privateKeyBundleV1.identityKey,
client.v1keys.identityKey,
clientFromV1Bundle.v1keys.identityKey,
)
assertEquals(
client.privateKeyBundleV1.preKeysList,
clientFromV1Bundle.privateKeyBundleV1.preKeysList,
client.v1keys.preKeysList,
clientFromV1Bundle.v1keys.preKeysList,
)
}

Expand Down Expand Up @@ -107,8 +107,8 @@ class ClientTest {
}
assertEquals(client.address, clientFromV1Bundle.address)
assertEquals(
client.privateKeyBundleV1.identityKey,
clientFromV1Bundle.privateKeyBundleV1.identityKey,
client.v1keys.identityKey,
clientFromV1Bundle.v1keys.identityKey,
)

runBlocking {
Expand Down Expand Up @@ -146,6 +146,31 @@ class ClientTest {
assertEquals(inboxId, client.inboxId)
}

@Test
fun testCreatesAV3OnlyClient() {
val key = SecureRandom().generateSeed(32)
val context = InstrumentationRegistry.getInstrumentation().targetContext
val fakeWallet = PrivateKeyBuilder()
val options = ClientOptions(
ClientOptions.Api(XMTPEnvironment.LOCAL, false),
enableV3 = true,
appContext = context,
dbEncryptionKey = key
)
val inboxId = runBlocking { Client.getOrCreateInboxId(options, fakeWallet.address) }
val client = runBlocking {
Client().createOrBuild(
account = fakeWallet,
options = options
)
}
runBlocking {
client.canMessageV3(listOf(client.address))[client.address]?.let { assert(it) }
}
assert(client.installationId.isNotEmpty())
assertEquals(inboxId, client.inboxId)
}

@Test
fun testCanDeleteDatabase() {
val key = SecureRandom().generateSeed(32)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class ConversationsTest {
val newWallet = PrivateKeyBuilder()
val newClient = runBlocking { Client().create(account = newWallet) }
val message = MessageV1Builder.buildEncode(
sender = newClient.privateKeyBundleV1,
sender = newClient.v1keys,
recipient = fixtures.aliceClient.v1keys.toPublicKeyBundle(),
message = TextCodec().encode(content = "hello").toByteArray(),
timestamp = created
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ class LocalInstrumentedTest {
private fun publishLegacyContact(client: Client) {
val contactBundle = Contact.ContactBundle.newBuilder().also {
it.v1 = it.v1.toBuilder().apply {
keyBundle = client.privateKeyBundleV1.toPublicKeyBundle()
keyBundle = client.v1keys.toPublicKeyBundle()
}.build()
}.build()
val envelope = Envelope.newBuilder().also {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ data class Fixtures(
fun publishLegacyContact(client: Client) {
val contactBundle = ContactBundle.newBuilder().also { builder ->
builder.v1 = builder.v1.toBuilder().also {
it.keyBundle = client.privateKeyBundleV1.toPublicKeyBundle()
it.keyBundle = client.v1keys.toPublicKeyBundle()
}.build()
}.build()
val envelope = Envelope.newBuilder().apply {
Expand Down
86 changes: 71 additions & 15 deletions library/src/main/java/org/xmtp/android/library/Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import java.time.Instant
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import kotlin.random.Random
import kotlin.random.nextULong
nplasterer marked this conversation as resolved.
Show resolved Hide resolved

typealias PublishResponse = org.xmtp.proto.message.api.v1.MessageApiOuterClass.PublishResponse
typealias QueryResponse = org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryResponse
Expand Down Expand Up @@ -83,10 +85,10 @@ data class ClientOptions(

class Client() {
lateinit var address: String
lateinit var privateKeyBundleV1: PrivateKeyBundleV1
lateinit var apiClient: ApiClient
lateinit var contacts: Contacts
lateinit var conversations: Conversations
var privateKeyBundleV1: PrivateKeyBundleV1? = null
var apiClient: ApiClient? = null
var logger: XMTPLogger = XMTPLogger()
val libXMTPVersion: String = getVersionInfo()
var installationId: String = ""
Expand Down Expand Up @@ -199,6 +201,23 @@ class Client() {
this.inboxId = inboxId
}

constructor(
address: String,
libXMTPClient: FfiXmtpClient,
dbPath: String,
installationId: String,
inboxId: String,
) : this() {
this.address = address
this.contacts = Contacts(client = this)
this.v3Client = libXMTPClient
this.conversations =
Conversations(client = this, libXMTPConversations = libXMTPClient.conversations())
this.dbPath = dbPath
this.installationId = installationId
this.inboxId = inboxId
}

suspend fun buildFrom(
bundle: PrivateKeyBundleV1,
options: ClientOptions? = null,
Expand Down Expand Up @@ -266,6 +285,39 @@ class Client() {
}
}

// This is a V3 only feature
suspend fun createOrBuild(
account: SigningKey,
options: ClientOptions,
): Client {
hasV2Client = false
val inboxId = getOrCreateInboxId(options, account.address)

return try {
val (libXMTPClient, dbPath) = ffiXmtpClient(
options,
account,
options.appContext,
null,
account.address,
inboxId
)

libXMTPClient?.let { client ->
Client(
account.address,
client,
dbPath,
client.installationId().toHex(),
client.inboxId()
)
} ?: throw XMTPException("Error creating V3 client: libXMTPClient is null")

} catch (e: Exception) {
throw XMTPException("Error creating V3 client: ${e.message}", e)
}
}

suspend fun buildFromBundle(
bundle: PrivateKeyBundle,
options: ClientOptions? = null,
Expand Down Expand Up @@ -318,7 +370,7 @@ class Client() {
options: ClientOptions,
account: SigningKey?,
appContext: Context?,
privateKeyBundleV1: PrivateKeyBundleV1,
privateKeyBundleV1: PrivateKeyBundleV1?,
address: String,
inboxId: String,
): Pair<FfiXmtpClient?, String> {
Expand Down Expand Up @@ -349,7 +401,7 @@ class Client() {
accountAddress = accountAddress,
inboxId = inboxId,
nonce = 0.toULong(),
legacySignedPrivateKeyProto = privateKeyBundleV1.toV2().identityKey.toByteArray(),
legacySignedPrivateKeyProto = privateKeyBundleV1?.toV2()?.identityKey?.toByteArray(),
historySyncUrl = options.historySyncUrl
)
} else {
Expand Down Expand Up @@ -437,7 +489,7 @@ class Client() {
if (legacy) {
val contactBundle = ContactBundle.newBuilder().also {
it.v1 = it.v1.toBuilder().also { v1Builder ->
v1Builder.keyBundle = privateKeyBundleV1.toPublicKeyBundle()
v1Builder.keyBundle = v1keys.toPublicKeyBundle()
}.build()
}.build()

Expand Down Expand Up @@ -477,11 +529,13 @@ class Client() {
}

suspend fun query(topic: Topic, pagination: Pagination? = null): QueryResponse {
return apiClient.queryTopic(topic = topic, pagination = pagination)
val client = apiClient ?: throw XMTPException("V2 only function")
return client.queryTopic(topic = topic, pagination = pagination)
}

suspend fun batchQuery(requests: List<QueryRequest>): BatchQueryResponse {
return apiClient.batchQuery(requests)
val client = apiClient ?: throw XMTPException("V2 only function")
return client.batchQuery(requests)
}

suspend fun subscribe(
Expand All @@ -495,7 +549,8 @@ class Client() {
request: FfiV2SubscribeRequest,
callback: FfiV2SubscriptionCallback,
): FfiV2Subscription {
return apiClient.subscribe(request, callback)
val client = apiClient ?: throw XMTPException("V2 only function")
nplasterer marked this conversation as resolved.
Show resolved Hide resolved
return client.subscribe(request, callback)
}

suspend fun fetchConversation(
Expand Down Expand Up @@ -533,13 +588,14 @@ class Client() {
suspend fun publish(envelopes: List<Envelope>) {
val authorized = AuthorizedIdentity(
address = address,
authorized = privateKeyBundleV1.identityKey.publicKey,
identity = privateKeyBundleV1.identityKey,
authorized = v1keys.identityKey.publicKey,
identity = v1keys.identityKey,
)
val client = apiClient ?: throw XMTPException("V2 only function")
nplasterer marked this conversation as resolved.
Show resolved Hide resolved
val authToken = authorized.createAuthToken()
apiClient.setAuthToken(authToken)
client.setAuthToken(authToken)

apiClient.publish(envelopes = envelopes)
client.publish(envelopes = envelopes)
}

suspend fun ensureUserContactPublished() {
Expand Down Expand Up @@ -670,11 +726,11 @@ class Client() {
}

val privateKeyBundle: PrivateKeyBundle
get() = PrivateKeyBundleBuilder.buildFromV1Key(privateKeyBundleV1)
get() = PrivateKeyBundleBuilder.buildFromV1Key(v1keys)

val v1keys: PrivateKeyBundleV1
get() = privateKeyBundleV1
get() = privateKeyBundleV1 ?: throw XMTPException("V2 only function")

val keys: PrivateKeyBundleV2
get() = privateKeyBundleV1.toV2()
get() = v1keys.toV2()
}
Comment on lines 720 to 728
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic all seems fine here, but these names seem very confusing for me personally. The names aren't new in this PR though, so no need to block the change.

an example improvement might be:
( we already have the nullable class field privateKeyBundleV1)

  • v1keys could instead be called something like nonNullPrivateKeyBundleV1
  • keys could instead be called privateKeyBundleV2

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya this one really thew me off as well something I will be happy to kill with V2.

Loading
Loading