Skip to content

Commit

Permalink
Fully test SCW functionality (#308)
Browse files Browse the repository at this point in the history
* add tests reproducing the SCW issues

* on latest bindings

* get all the tests passing

* Dump the schema

* dumpt he new binaries

* fix up messaging ordering

* fix up linter
  • Loading branch information
nplasterer authored Oct 23, 2024
1 parent 492c797 commit 9c7c3ad
Show file tree
Hide file tree
Showing 10 changed files with 679 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,277 @@ package org.xmtp.android.library

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.xmtp.android.library.messages.MessageDeliveryStatus
import org.xmtp.android.library.messages.PrivateKey
import org.xmtp.android.library.messages.PrivateKeyBuilder
import org.xmtp.android.library.messages.walletAddress

@RunWith(AndroidJUnit4::class)
class SmartContractWalletTest {
@Test
fun testCanCreateASCW() {
val key = byteArrayOf(
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F
)
val context = InstrumentationRegistry.getInstrumentation().targetContext
val davonSCW = FakeSCWWallet.generate()
val options = ClientOptions(
ClientOptions.Api(XMTPEnvironment.LOCAL, false),
enableV3 = true,
appContext = context,
dbEncryptionKey = key
)
val davonSCWClient = runBlocking {
Client().createV3(
account = davonSCW,
options = options
companion object {
private lateinit var davonSCW: FakeSCWWallet
private lateinit var davonSCWClient: Client
private lateinit var eriSCW: FakeSCWWallet
private lateinit var eriSCWClient: Client
private lateinit var options: ClientOptions
private lateinit var boV3Wallet: PrivateKeyBuilder
private lateinit var boV3: PrivateKey
private lateinit var boV3Client: Client

@BeforeClass
@JvmStatic
fun setUpClass() {
val key = byteArrayOf(
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F
)
val context = InstrumentationRegistry.getInstrumentation().targetContext
options = ClientOptions(
ClientOptions.Api(XMTPEnvironment.LOCAL, false),
enableV3 = true,
appContext = context,
dbEncryptionKey = key
)

// EOA
boV3Wallet = PrivateKeyBuilder()
boV3 = boV3Wallet.getPrivateKey()
boV3Client = runBlocking {
Client().createV3(
account = boV3Wallet,
options = options
)
}

// SCW
davonSCW = FakeSCWWallet.generate(ANVIL_TEST_PRIVATE_KEY_1)
davonSCWClient = runBlocking {
Client().createV3(
account = davonSCW,
options = options
)
}

// SCW
eriSCW = FakeSCWWallet.generate(ANVIL_TEST_PRIVATE_KEY_2)
eriSCWClient = runBlocking {
Client().createV3(
account = eriSCW,
options = options
)
}
}
}

@Test
fun testCanBuildASCW() {
val davonSCWClient2 = runBlocking {
Client().buildV3(
address = davonSCW.address,
chainId = davonSCW.chainId,
options = options
)
}

assertEquals(davonSCWClient.inboxId, davonSCWClient2.inboxId)
}

@Test
fun testsCanCreateGroup() {
val group1 = runBlocking {
boV3Client.conversations.newGroup(
listOf(
davonSCW.walletAddress,
eriSCW.walletAddress
)
)
}
val group2 = runBlocking {
davonSCWClient.conversations.newGroup(
listOf(
boV3.walletAddress,
eriSCW.walletAddress
)
)
}

assertEquals(
runBlocking { group1.members().map { it.inboxId }.sorted() },
listOf(davonSCWClient.inboxId, boV3Client.inboxId, eriSCWClient.inboxId).sorted()
)
assertEquals(
runBlocking { group2.members().map { it.addresses.first() }.sorted() },
listOf(davonSCWClient.address, boV3Client.address, eriSCWClient.address).sorted()
)
}

@Test
fun testsCanSendMessages() {
val boGroup = runBlocking {
boV3Client.conversations.newGroup(
listOf(
davonSCW.walletAddress,
eriSCW.walletAddress
)
)
}
runBlocking { boGroup.send("howdy") }
val messageId = runBlocking { boGroup.send("gm") }
runBlocking { boGroup.sync() }
assertEquals(boGroup.messages().first().body, "gm")
assertEquals(boGroup.messages().first().id, messageId)
assertEquals(boGroup.messages().first().deliveryStatus, MessageDeliveryStatus.PUBLISHED)
assertEquals(boGroup.messages().size, 3)

runBlocking { davonSCWClient.conversations.syncGroups() }
val davonGroup = runBlocking { davonSCWClient.conversations.listGroups().last() }
runBlocking { davonGroup.sync() }
assertEquals(davonGroup.messages().size, 2)
assertEquals(davonGroup.messages().first().body, "gm")
runBlocking { davonGroup.send("from davon") }

runBlocking { eriSCWClient.conversations.syncGroups() }
val eriGroup = runBlocking { davonSCWClient.findGroup(davonGroup.id) }
runBlocking { eriGroup?.sync() }
assertEquals(eriGroup?.messages()?.size, 3)
assertEquals(eriGroup?.messages()?.first()?.body, "from davon")
runBlocking { eriGroup?.send("from eri") }
}

@Test
fun testGroupConsent() {
runBlocking {
val davonGroup = runBlocking {
davonSCWClient.conversations.newGroup(
listOf(
boV3.walletAddress,
eriSCW.walletAddress
)
)
}
assert(davonSCWClient.contacts.isGroupAllowed(davonGroup.id))
assertEquals(davonGroup.consentState(), ConsentState.ALLOWED)

davonSCWClient.contacts.denyGroups(listOf(davonGroup.id))
assert(davonSCWClient.contacts.isGroupDenied(davonGroup.id))
assertEquals(davonGroup.consentState(), ConsentState.DENIED)

davonGroup.updateConsentState(ConsentState.ALLOWED)
assert(davonSCWClient.contacts.isGroupAllowed(davonGroup.id))
assertEquals(davonGroup.consentState(), ConsentState.ALLOWED)
}
}

@Test
fun testCanAllowAndDenyInboxId() {
runBlocking {
val davonGroup = runBlocking {
davonSCWClient.conversations.newGroup(
listOf(
boV3.walletAddress,
eriSCW.walletAddress
)
)
}
assert(!davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId))
assert(!davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId))

davonSCWClient.contacts.allowInboxes(listOf(boV3Client.inboxId))
var caroMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId }
assertEquals(caroMember!!.consentState, ConsentState.ALLOWED)

assert(davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId))
assert(!davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId))
assert(davonSCWClient.contacts.isAllowed(boV3Client.address))
assert(!davonSCWClient.contacts.isDenied(boV3Client.address))

davonSCWClient.contacts.denyInboxes(listOf(boV3Client.inboxId))
caroMember = davonGroup.members().firstOrNull { it.inboxId == boV3Client.inboxId }
assertEquals(caroMember!!.consentState, ConsentState.DENIED)

assert(!davonSCWClient.contacts.isInboxAllowed(boV3Client.inboxId))
assert(davonSCWClient.contacts.isInboxDenied(boV3Client.inboxId))

davonSCWClient.contacts.allow(listOf(eriSCWClient.address))
assert(davonSCWClient.contacts.isAllowed(eriSCWClient.address))
assert(!davonSCWClient.contacts.isDenied(eriSCWClient.address))
assert(davonSCWClient.contacts.isInboxAllowed(eriSCWClient.inboxId))
assert(!davonSCWClient.contacts.isInboxDenied(eriSCWClient.inboxId))
}
}

@Test
fun testCanStreamAllMessages() {
val group = runBlocking {
davonSCWClient.conversations.newGroup(
listOf(
boV3.walletAddress,
eriSCW.walletAddress
)
)
}
val group2 = runBlocking {
boV3Client.conversations.newGroup(
listOf(
davonSCW.walletAddress,
eriSCW.walletAddress
)
)
}
runBlocking { davonSCWClient.conversations.syncGroups() }

val allMessages = mutableListOf<DecodedMessage>()

val job = CoroutineScope(Dispatchers.IO).launch {
try {
davonSCWClient.conversations.streamAllGroupMessages()
.collect { message ->
allMessages.add(message)
}
} catch (e: Exception) {
}
}
Thread.sleep(1000)
runBlocking {
group.send("hi")
group2.send("hi")
}
Thread.sleep(1000)
assertEquals(2, allMessages.size)
job.cancel()
}

@Test
fun testCanStreamGroups() {
val allMessages = mutableListOf<String>()

val job = CoroutineScope(Dispatchers.IO).launch {
try {
davonSCWClient.conversations.streamGroups()
.collect { message ->
allMessages.add(message.topic)
}
} catch (e: Exception) {
}
}
Thread.sleep(1000)

runBlocking {
eriSCWClient.conversations.newGroup(listOf(boV3.walletAddress, davonSCW.walletAddress))
boV3Client.conversations.newGroup(listOf(eriSCW.walletAddress, davonSCW.walletAddress))
}

Thread.sleep(1000)
assertEquals(2, allMessages.size)
job.cancel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ class FakeWallet : SigningKey {
get() = privateKey.walletAddress
}

private const val ANVIL_TEST_PRIVATE_KEY =
const val ANVIL_TEST_PRIVATE_KEY_1 =
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
const val ANVIL_TEST_PRIVATE_KEY_2 =
"59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
private const val ANVIL_TEST_PORT = "http://10.0.2.2:8545"

class FakeSCWWallet : SigningKey {
private var web3j: Web3j = Web3j.build(HttpService(ANVIL_TEST_PORT))
private val contractDeployerCredentials: Credentials =
Credentials.create(ANVIL_TEST_PRIVATE_KEY)
private val web3j: Web3j = Web3j.build(HttpService(ANVIL_TEST_PORT))
private var contractDeployerCredentials: Credentials? = null
var walletAddress: String = ""

override val address: String
Expand All @@ -73,8 +74,9 @@ class FakeSCWWallet : SigningKey {
override var chainId: Long? = 31337L

companion object {
fun generate(): FakeSCWWallet {
fun generate(privateKey: String): FakeSCWWallet {
return FakeSCWWallet().apply {
contractDeployerCredentials = Credentials.create(privateKey)
createSmartContractWallet()
}
}
Expand All @@ -91,7 +93,7 @@ class FakeSCWWallet : SigningKey {
val replaySafeHash = smartWallet.replaySafeHash(digest).send()

val signature =
Sign.signMessage(replaySafeHash, contractDeployerCredentials.ecKeyPair, false)
Sign.signMessage(replaySafeHash, contractDeployerCredentials!!.ecKeyPair, false)
val signatureBytes = signature.r + signature.s + signature.v
val tokens = listOf(
Uint(BigInteger.ZERO),
Expand Down Expand Up @@ -119,7 +121,13 @@ class FakeSCWWallet : SigningKey {
).send()

val ownerAddress = ByteArray(32) { 0 }.apply {
System.arraycopy(contractDeployerCredentials.address.hexToByteArray(), 0, this, 12, 20)
System.arraycopy(
contractDeployerCredentials!!.address.hexToByteArray(),
0,
this,
12,
20
)
}
val owners = listOf(ownerAddress)
val nonce = BigInteger.ZERO
Expand Down
6 changes: 3 additions & 3 deletions library/src/main/java/libxmtp-version.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Version: 6937f233
Branch: np/fix-ffi-bindings
Date: 2024-10-16 17:50:35 +0000
Version: 9331eb7d
Branch: main
Date: 2024-10-23 16:57:08 +0000
7 changes: 2 additions & 5 deletions library/src/main/java/org/xmtp/android/library/Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,7 @@ class Client() {
): Client {
this.hasV2Client = false
val clientOptions = options ?: ClientOptions(enableV3 = true)
val accountAddress =
if (account.chainId != null) "eip155:${account.chainId}:${account.address.lowercase()}" else account.address.lowercase()
val accountAddress = account.address.lowercase()
return try {
initializeV3Client(accountAddress, clientOptions, account)
} catch (e: Exception) {
Expand All @@ -336,13 +335,11 @@ class Client() {
// Function to build a V3 client without a signing key (using only address (& chainId for SCW))
suspend fun buildV3(
address: String,
chainId: Long? = null,
options: ClientOptions? = null,
): Client {
this.hasV2Client = false
val clientOptions = options ?: ClientOptions(enableV3 = true)
val accountAddress =
if (chainId != null) "eip155:$chainId:${address.lowercase()}" else address.lowercase()
val accountAddress = address.lowercase()
return try {
initializeV3Client(accountAddress, clientOptions)
} catch (e: Exception) {
Expand Down
Loading

0 comments on commit 9c7c3ad

Please sign in to comment.