Skip to content

Commit ad4884d

Browse files
authored
Implement persistent Allow State (#125)
* bump the proto version * dump the updated rust bindings * add the allow list implementation * update the parsing * write tests for it * update the way the client is created * update the topic to the new agreed upon topic name * try to update docker compose * fix up the unit tests * it is decrypt not encrypt * fix up the linter issues * more fixes for contact * more fixes for linter * fix up test not running properly * make it easier to check * fix linter again
1 parent 9561552 commit ad4884d

File tree

16 files changed

+539
-65
lines changed

16 files changed

+539
-65
lines changed

dev/local/docker-compose.yml

+4-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
version: "3.8"
21
services:
3-
wakunode:
2+
waku-node:
43
image: xmtp/node-go:latest
4+
platform: linux/amd64
55
environment:
66
- GOWAKU-NODEKEY=8a30dcb604b0b53627a5adc054dbf434b446628d4bd1eccc681d223f0550ce67
77
command:
@@ -12,22 +12,10 @@ services:
1212
- --api.authn.enable
1313
ports:
1414
- 9001:9001
15-
- 5555:5555 # http message API
16-
- 5556:5556 # grpc message API
15+
- 5555:5555
1716
depends_on:
1817
- db
19-
healthcheck:
20-
test: [ "CMD", "lsof", "-i", ":5556" ]
21-
interval: 3s
22-
timeout: 10s
23-
retries: 5
2418
db:
2519
image: postgres:13
2620
environment:
27-
POSTGRES_PASSWORD: xmtp
28-
js:
29-
restart: always
30-
depends_on:
31-
wakunode:
32-
condition: service_healthy
33-
build: ./test
21+
POSTGRES_PASSWORD: xmtp

library/build.gradle

+7-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ dokkaGfmPartial {
1212
outputDirectory.set(file("build/docs/partial"))
1313
}
1414

15+
ktlint {
16+
filter {
17+
exclude { it.file.path.contains("xmtp_dh") }
18+
}
19+
}
20+
1521
android {
1622
namespace 'org.xmtp.android.library'
1723
compileSdk 33
@@ -80,7 +86,7 @@ dependencies {
8086
implementation 'org.web3j:crypto:5.0.0'
8187
implementation "net.java.dev.jna:jna:5.13.0@aar"
8288
api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3'
83-
api 'org.xmtp:proto-kotlin:3.24.1'
89+
api 'org.xmtp:proto-kotlin:3.31.0'
8490

8591
testImplementation 'junit:junit:4.13.2'
8692
androidTestImplementation 'app.cash.turbine:turbine:0.12.1'

library/src/test/java/org/xmtp/android/library/ClientTest.kt library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt

+20
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package org.xmtp.android.library
22

3+
import androidx.test.ext.junit.runners.AndroidJUnit4
34
import org.junit.Assert.assertEquals
45
import org.junit.Test
6+
import org.junit.runner.RunWith
57
import org.xmtp.android.library.messages.PrivateKeyBuilder
8+
import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder
9+
import org.xmtp.android.library.messages.generate
10+
import org.xmtp.proto.message.contents.PrivateKeyOuterClass
611

12+
@RunWith(AndroidJUnit4::class)
713
class ClientTest {
814
@Test
915
fun testTakesAWallet() {
@@ -20,6 +26,20 @@ class ClientTest {
2026
assert(preKey?.publicKey?.hasSignature() ?: false)
2127
}
2228

29+
@Test
30+
fun testSerialization() {
31+
val wallet = PrivateKeyBuilder()
32+
val v1 =
33+
PrivateKeyOuterClass.PrivateKeyBundleV1.newBuilder().build().generate(wallet = wallet)
34+
val encodedData = PrivateKeyBundleV1Builder.encodeData(v1)
35+
val v1Copy = PrivateKeyBundleV1Builder.fromEncodedData(encodedData)
36+
val client = Client().buildFrom(v1Copy)
37+
assertEquals(
38+
wallet.address,
39+
client.address
40+
)
41+
}
42+
2343
@Test
2444
fun testCanBeCreatedWithBundle() {
2545
val fakeWallet = PrivateKeyBuilder()

library/src/test/java/org/xmtp/android/library/ContactsTest.kt library/src/androidTest/java/org/xmtp/android/library/ContactsTest.kt

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package org.xmtp.android.library
22

3+
import androidx.test.ext.junit.runners.AndroidJUnit4
34
import org.junit.Assert.assertEquals
45
import org.junit.Test
6+
import org.junit.runner.RunWith
57
import org.xmtp.android.library.messages.walletAddress
6-
8+
@RunWith(AndroidJUnit4::class)
79
class ContactsTest {
810

911
@Test
@@ -35,4 +37,34 @@ class ContactsTest {
3537
}
3638
assert(fixtures.aliceClient.contacts.has(fixtures.bob.walletAddress))
3739
}
40+
41+
@Test
42+
fun testAllowAddress() {
43+
val fixtures = fixtures()
44+
45+
val contacts = fixtures.bobClient.contacts
46+
var result = contacts.isAllowed(fixtures.alice.walletAddress)
47+
48+
assert(!result)
49+
50+
contacts.allow(listOf(fixtures.alice.walletAddress))
51+
52+
result = contacts.isAllowed(fixtures.alice.walletAddress)
53+
assert(result)
54+
}
55+
56+
@Test
57+
fun testBlockAddress() {
58+
val fixtures = fixtures()
59+
60+
val contacts = fixtures.bobClient.contacts
61+
var result = contacts.isAllowed(fixtures.alice.walletAddress)
62+
63+
assert(!result)
64+
65+
contacts.block(listOf(fixtures.alice.walletAddress))
66+
67+
result = contacts.isBlocked(fixtures.alice.walletAddress)
68+
assert(result)
69+
}
3870
}

library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt

+30
Original file line numberDiff line numberDiff line change
@@ -713,4 +713,34 @@ class ConversationTest {
713713
assertEquals(1, messages.size)
714714
assertEquals("hi", messages[0].content())
715715
}
716+
717+
@Test
718+
fun testCanHaveAllowState() {
719+
val bobConversation = bobClient.conversations.newConversation(alice.walletAddress, null)
720+
val isAllowed = bobConversation.allowState() == AllowState.ALLOW
721+
722+
// Conversations you start should start as allowed
723+
assertTrue(isAllowed)
724+
725+
val aliceConversation = aliceClient.conversations.list()[0]
726+
val isUnknown = aliceConversation.allowState() == AllowState.UNKNOWN
727+
728+
// Conversations started with you should start as unknown
729+
assertTrue(isUnknown)
730+
731+
aliceClient.contacts.allow(listOf(bob.walletAddress))
732+
733+
val isBobAllowed = aliceConversation.allowState() == AllowState.ALLOW
734+
assertTrue(isBobAllowed)
735+
736+
val aliceClient2 = Client().create(aliceWallet, fakeApiClient)
737+
val aliceConversation2 = aliceClient2.conversations.list()[0]
738+
739+
aliceClient2.contacts.refreshAllowList()
740+
741+
// Allow state should sync across clients
742+
val isBobAllowed2 = aliceConversation2.allowState() == AllowState.ALLOW
743+
744+
assertTrue(isBobAllowed2)
745+
}
716746
}

library/src/androidTest/java/org/xmtp/android/library/LocalInstrumentedTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ class LocalInstrumentedTest {
277277

278278
@OptIn(ExperimentalCoroutinesApi::class)
279279
@Test
280-
fun testStreamAllMessagesWorksWithIntros() = runBlocking {
280+
fun testStreamAllMessagesWorksWithIntros() {
281281
val bob = PrivateKeyBuilder()
282282
val alice = PrivateKeyBuilder()
283283
val clientOptions =

library/src/main/java/org/xmtp/android/library/Contacts.kt

+147-1
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,161 @@ package org.xmtp.android.library
33
import kotlinx.coroutines.runBlocking
44
import org.xmtp.android.library.messages.ContactBundle
55
import org.xmtp.android.library.messages.ContactBundleBuilder
6+
import org.xmtp.android.library.messages.EnvelopeBuilder
67
import org.xmtp.android.library.messages.Topic
78
import org.xmtp.android.library.messages.walletAddress
9+
import org.xmtp.proto.message.contents.PrivatePreferences.PrivatePreferencesAction
10+
import java.util.Date
11+
12+
typealias MessageType = PrivatePreferencesAction.MessageTypeCase
13+
14+
enum class AllowState {
15+
ALLOW,
16+
BLOCK,
17+
UNKNOWN
18+
}
19+
data class AllowListEntry(
20+
val value: String,
21+
val entryType: EntryType,
22+
val permissionType: AllowState,
23+
) {
24+
enum class EntryType {
25+
ADDRESS
26+
}
27+
28+
companion object {
29+
fun address(
30+
address: String,
31+
type: AllowState = AllowState.UNKNOWN,
32+
): AllowListEntry {
33+
return AllowListEntry(address, EntryType.ADDRESS, type)
34+
}
35+
}
36+
37+
val key: String
38+
get() = "${entryType.name}-$value"
39+
}
40+
41+
class AllowList(val client: Client) {
42+
private val entries: MutableMap<String, AllowState> = mutableMapOf()
43+
private val publicKey =
44+
client.privateKeyBundleV1.identityKey.publicKey.secp256K1Uncompressed.bytes
45+
private val privateKey = client.privateKeyBundleV1.identityKey.secp256K1.bytes
46+
47+
@OptIn(ExperimentalUnsignedTypes::class)
48+
private val identifier: String = uniffi.xmtp_dh.generatePrivatePreferencesTopicIdentifier(
49+
privateKey.toByteArray().toUByteArray().toList()
50+
)
51+
52+
@OptIn(ExperimentalUnsignedTypes::class)
53+
suspend fun load(): AllowList {
54+
val envelopes = client.query(Topic.preferenceList(identifier))
55+
val allowList = AllowList(client)
56+
val preferences: MutableList<PrivatePreferencesAction> = mutableListOf()
57+
58+
for (envelope in envelopes.envelopesList) {
59+
val payload = uniffi.xmtp_dh.eciesDecryptK256Sha3256(
60+
publicKey.toByteArray().toUByteArray().toList(),
61+
privateKey.toByteArray().toUByteArray().toList(),
62+
envelope.message.toByteArray().toUByteArray().toList()
63+
)
64+
65+
preferences.add(
66+
PrivatePreferencesAction.parseFrom(
67+
payload.toUByteArray().toByteArray()
68+
)
69+
)
70+
}
71+
72+
preferences.iterator().forEach { preference ->
73+
preference.allow?.walletAddressesList?.forEach { address ->
74+
allowList.allow(address)
75+
}
76+
preference.block?.walletAddressesList?.forEach { address ->
77+
allowList.block(address)
78+
}
79+
}
80+
return allowList
81+
}
82+
83+
@OptIn(ExperimentalUnsignedTypes::class)
84+
fun publish(entry: AllowListEntry) {
85+
val payload = PrivatePreferencesAction.newBuilder().also {
86+
when (entry.permissionType) {
87+
AllowState.ALLOW -> it.setAllow(PrivatePreferencesAction.Allow.newBuilder().addWalletAddresses(entry.value))
88+
AllowState.BLOCK -> it.setBlock(PrivatePreferencesAction.Block.newBuilder().addWalletAddresses(entry.value))
89+
AllowState.UNKNOWN -> it.clearMessageType()
90+
}
91+
}.build()
92+
93+
val message = uniffi.xmtp_dh.eciesEncryptK256Sha3256(
94+
publicKey.toByteArray().toUByteArray().toList(),
95+
privateKey.toByteArray().toUByteArray().toList(),
96+
payload.toByteArray().toUByteArray().toList()
97+
)
98+
99+
val envelope = EnvelopeBuilder.buildFromTopic(
100+
Topic.preferenceList(identifier),
101+
Date(),
102+
ByteArray(message.size) { message[it].toByte() }
103+
)
104+
105+
client.publish(listOf(envelope))
106+
}
107+
108+
fun allow(address: String): AllowListEntry {
109+
entries[AllowListEntry.address(address).key] = AllowState.ALLOW
110+
111+
return AllowListEntry.address(address, AllowState.ALLOW)
112+
}
113+
114+
fun block(address: String): AllowListEntry {
115+
entries[AllowListEntry.address(address).key] = AllowState.BLOCK
116+
117+
return AllowListEntry.address(address, AllowState.BLOCK)
118+
}
119+
120+
fun state(address: String): AllowState {
121+
val state = entries[AllowListEntry.address(address).key]
122+
123+
return state ?: AllowState.UNKNOWN
124+
}
125+
}
8126

9127
data class Contacts(
10128
var client: Client,
11129
val knownBundles: MutableMap<String, ContactBundle> = mutableMapOf(),
12-
val hasIntroduced: MutableMap<String, Boolean> = mutableMapOf()
130+
val hasIntroduced: MutableMap<String, Boolean> = mutableMapOf(),
13131
) {
14132

133+
var allowList: AllowList = AllowList(client)
134+
135+
fun refreshAllowList() {
136+
runBlocking {
137+
allowList = AllowList(client).load()
138+
}
139+
}
140+
141+
fun isAllowed(address: String): Boolean {
142+
return allowList.state(address) == AllowState.ALLOW
143+
}
144+
145+
fun isBlocked(address: String): Boolean {
146+
return allowList.state(address) == AllowState.BLOCK
147+
}
148+
149+
fun allow(addresses: List<String>) {
150+
for (address in addresses) {
151+
AllowList(client).publish(allowList.allow(address))
152+
}
153+
}
154+
155+
fun block(addresses: List<String>) {
156+
for (address in addresses) {
157+
AllowList(client).publish(allowList.block(address))
158+
}
159+
}
160+
15161
fun has(peerAddress: String): Boolean =
16162
knownBundles[peerAddress] != null
17163

library/src/main/java/org/xmtp/android/library/Conversation.kt

+8
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ sealed class Conversation {
6161
}
6262
}
6363

64+
fun allowState(): AllowState {
65+
val client: Client = when (this) {
66+
is V1 -> conversationV1.client
67+
is V2 -> conversationV2.client
68+
}
69+
return client.contacts.allowList.state(address = peerAddress)
70+
}
71+
6472
fun toTopicData(): TopicData {
6573
val data = TopicData.newBuilder()
6674
.setCreatedNs(createdAt.time * 1_000_000)

library/src/main/java/org/xmtp/android/library/Conversations.kt

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ data class Conversations(
142142
invitation = invitation,
143143
header = sealedInvitation.v1.header
144144
)
145+
client.contacts.allow(addresses = listOf(peerAddress))
145146
val conversation = Conversation.V2(conversationV2)
146147
conversationsByTopic[conversation.topic] = conversation
147148
return conversation

library/src/main/java/org/xmtp/android/library/messages/Topic.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ sealed class Topic {
77
data class userInvite(val address: String?) : Topic()
88
data class directMessageV1(val address1: String?, val address2: String?) : Topic()
99
data class directMessageV2(val addresses: String?) : Topic()
10-
data class groupInvite(val address: String?) : Topic()
10+
data class preferenceList(val identifier: String?) : Topic()
1111

1212
val description: String
1313
get() {
@@ -22,7 +22,7 @@ sealed class Topic {
2222
wrap("dm-${addresses.joinToString(separator = "-")}")
2323
}
2424
is directMessageV2 -> wrap("m-$addresses")
25-
is groupInvite -> wrap("groupInvite-$address")
25+
is preferenceList -> wrap("pppp-$identifier")
2626
}
2727
}
2828

0 commit comments

Comments
 (0)