Skip to content

Commit

Permalink
Smart contract wallet support (#304)
Browse files Browse the repository at this point in the history
* bump version

* get on the latest bindings

* add the defaults

* bump version

* Fix kt lint error that was blocking build

* allow block number to be optional

* bump version again

* write a test for it

* probably just want the bytes that were passed directly

* bump

* bump the lib as well

* add real smart contract wallet test

* add the binary files

* need to fix the signature issue

* make the signing key optional

* get on the latest version

* new binaries

* a few more tweaks to the sign functions

* maybe getting closer

* Fix failing SCW test

* Fix anvil command

* dump the bindings again

* update the client to create and a seperate to build

* get the tests cleaned up

* remove the read me

* dumpt he v

* make optional

* get all the tests working

* fix the linter

* rename

* add other types to the wallet for future identities

---------

Co-authored-by: koleok <[email protected]>
Co-authored-by: Nicholas Molnar <[email protected]>
  • Loading branch information
3 people authored Oct 22, 2024
1 parent a7f2e3c commit 492c797
Show file tree
Hide file tree
Showing 27 changed files with 3,573 additions and 1,337 deletions.
10 changes: 10 additions & 0 deletions dev/local/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ services:
validation:
image: ghcr.io/xmtp/mls-validation-service:main
platform: linux/amd64
environment:
ANVIL_URL: "http://anvil:8545"

anvil:
image: ghcr.io/foundry-rs/foundry
platform: linux/amd64
entrypoint: ["anvil", "--host", "0.0.0.0"]
working_dir: /anvil
ports:
- 8545:8545

db:
image: postgres:13
Expand Down
6 changes: 4 additions & 2 deletions library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,18 @@ dependencies {
implementation 'io.grpc:grpc-okhttp:1.62.2'
implementation 'io.grpc:grpc-protobuf-lite:1.62.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0'
implementation 'org.web3j:crypto:5.0.0'
implementation 'org.web3j:crypto:4.9.4'
implementation "net.java.dev.jna:jna:5.14.0@aar"
api 'com.google.protobuf:protobuf-kotlin-lite:3.22.3'
api 'org.xmtp:proto-kotlin:3.62.1'
api 'org.xmtp:proto-kotlin:3.71.0'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'app.cash.turbine:turbine:1.1.0'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'org.web3j:core:4.9.4'
androidTestImplementation 'org.web3j:contracts:4.9.4'
}

afterEvaluate {
Expand Down
5 changes: 5 additions & 0 deletions library/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:networkSecurityConfig="@xml/network_security_config">
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ class ClientTest {
)
val inboxId = runBlocking { Client.getOrCreateInboxId(options, fakeWallet.address) }
val client = runBlocking {
Client().createOrBuild(
Client().createV3(
account = fakeWallet,
options = options
)
Expand All @@ -169,6 +169,18 @@ class ClientTest {
}
assert(client.installationId.isNotEmpty())
assertEquals(inboxId, client.inboxId)

val sameClient = runBlocking {
Client().buildV3(
address = fakeWallet.address,
options = options
)
}
runBlocking {
client.canMessageV3(listOf(sameClient.address))[sameClient.address]?.let { assert(it) }
}
assert(sameClient.installationId.isNotEmpty())
assertEquals(client.inboxId, sameClient.inboxId)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.xmtp.android.library

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

@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
)
}
val davonSCWClient2 = runBlocking {
Client().buildV3(
address = davonSCW.address,
chainId = davonSCW.chainId,
options = options
)
}

assertEquals(davonSCWClient.inboxId, davonSCWClient2.inboxId)
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
package org.xmtp.android.library

import kotlinx.coroutines.runBlocking
import org.web3j.abi.FunctionEncoder
import org.web3j.abi.datatypes.DynamicBytes
import org.web3j.abi.datatypes.Uint
import org.web3j.crypto.Credentials
import org.web3j.crypto.Sign
import org.web3j.protocol.Web3j
import org.web3j.protocol.http.HttpService
import org.web3j.tx.gas.DefaultGasProvider
import org.web3j.utils.Numeric
import org.xmtp.android.library.artifact.CoinbaseSmartWallet
import org.xmtp.android.library.artifact.CoinbaseSmartWalletFactory
import org.xmtp.android.library.messages.ContactBundle
import org.xmtp.android.library.messages.Envelope
import org.xmtp.android.library.messages.PrivateKey
import org.xmtp.android.library.messages.PrivateKeyBuilder
import org.xmtp.android.library.messages.Signature
import org.xmtp.android.library.messages.Topic
import org.xmtp.android.library.messages.ethHash
import org.xmtp.android.library.messages.toPublicKeyBundle
import org.xmtp.android.library.messages.walletAddress
import java.math.BigInteger
import java.util.Date

class FakeWallet : SigningKey {
Expand Down Expand Up @@ -41,6 +54,87 @@ class FakeWallet : SigningKey {
get() = privateKey.walletAddress
}

private const val ANVIL_TEST_PRIVATE_KEY =
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
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)
var walletAddress: String = ""

override val address: String
get() = walletAddress

override val type: WalletType
get() = WalletType.SCW

override var chainId: Long? = 31337L

companion object {
fun generate(): FakeSCWWallet {
return FakeSCWWallet().apply {
createSmartContractWallet()
}
}
}

override suspend fun signSCW(message: String): ByteArray {
val smartWallet = CoinbaseSmartWallet.load(
walletAddress,
web3j,
contractDeployerCredentials,
DefaultGasProvider()
)
val digest = Signature.newBuilder().build().ethHash(message)
val replaySafeHash = smartWallet.replaySafeHash(digest).send()

val signature =
Sign.signMessage(replaySafeHash, contractDeployerCredentials.ecKeyPair, false)
val signatureBytes = signature.r + signature.s + signature.v
val tokens = listOf(
Uint(BigInteger.ZERO),
DynamicBytes(signatureBytes)
)
val encoded = FunctionEncoder.encodeConstructor(tokens)
val encodedBytes = Numeric.hexStringToByteArray(encoded)

return encodedBytes
}

private fun createSmartContractWallet() {
val smartWalletContract = CoinbaseSmartWallet.deploy(
web3j,
contractDeployerCredentials,
DefaultGasProvider()
).send()

val factory = CoinbaseSmartWalletFactory.deploy(
web3j,
contractDeployerCredentials,
DefaultGasProvider(),
BigInteger.ZERO,
smartWalletContract.contractAddress
).send()

val ownerAddress = ByteArray(32) { 0 }.apply {
System.arraycopy(contractDeployerCredentials.address.hexToByteArray(), 0, this, 12, 20)
}
val owners = listOf(ownerAddress)
val nonce = BigInteger.ZERO

val transactionReceipt = factory.createAccount(owners, nonce, BigInteger.ZERO).send()
val smartWalletAddress = factory.getAddress(owners, nonce).send()

if (transactionReceipt.isStatusOK) {
walletAddress = smartWalletAddress
} else {
throw Exception("Transaction failed: ${transactionReceipt.status}")
}
}
}

data class Fixtures(
val clientOptions: ClientOptions? = ClientOptions(
ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class V3ClientTest {
boV3Wallet = PrivateKeyBuilder()
boV3 = boV3Wallet.getPrivateKey()
boV3Client = runBlocking {
Client().createOrBuild(
Client().createV3(
account = boV3Wallet,
options = ClientOptions(
ClientOptions.Api(XMTPEnvironment.LOCAL, false),
Expand Down

Large diffs are not rendered by default.

Loading

0 comments on commit 492c797

Please sign in to comment.