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

V3 Frames support #320

Merged
merged 31 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2055f48
remove v1 and v2 conversations
nplasterer Nov 3, 2024
7baa420
update all the tests to compile
nplasterer Nov 3, 2024
ed5b11a
remove decrypted messages and only use decoded
nplasterer Nov 3, 2024
b8ae9b3
remove the ability to create a v2 client entirely
nplasterer Nov 3, 2024
e3eab59
remove contacts and rename to preferences
nplasterer Nov 3, 2024
b9e8078
Merge branch 'np/remove-contacts' of https://github.com/xmtp/xmtp-and…
nplasterer Nov 3, 2024
c1eaa3c
remove a bunch of classes no longer needed
nplasterer Nov 3, 2024
89f0e6b
get all the tests compiling
nplasterer Nov 3, 2024
cd8b714
fix up the wallet address
nplasterer Nov 3, 2024
f508186
more clean up
nplasterer Nov 3, 2024
6e28245
update the example to be v3 only
nplasterer Nov 3, 2024
b77670e
bring back the frames signer
nplasterer Nov 6, 2024
8b26b65
Merge branch 'main' of https://github.com/xmtp/xmtp-android into np/f…
nplasterer Nov 7, 2024
d90e994
add back the test
nplasterer Nov 7, 2024
2f9b43e
Merge branch 'main' into np/frames-signer
nplasterer Nov 19, 2024
0a21656
Merge branch 'main' into np/frames-signer
nplasterer Nov 19, 2024
0c28805
update the frames client to use the new signer
nplasterer Nov 19, 2024
375a295
Merge branch 'main' of https://github.com/xmtp/xmtp-android into np/f…
nplasterer Nov 20, 2024
255788a
try and get it working
nplasterer Nov 20, 2024
e31f08b
Merge branch 'main' of https://github.com/xmtp/xmtp-android into np/f…
nplasterer Nov 27, 2024
c502b65
frames client and inboxId
nplasterer Dec 1, 2024
62971f8
Merge branch 'main' of https://github.com/xmtp/xmtp-android into np/f…
nplasterer Dec 16, 2024
847165b
try and get closer on frames validation
nplasterer Dec 16, 2024
460ae9c
Merge branch 'main' of https://github.com/xmtp/xmtp-android into np/f…
nplasterer Dec 16, 2024
a8a727f
Merge branch 'main' of https://github.com/xmtp/xmtp-android into np/f…
nplasterer Dec 17, 2024
8550d9c
Merge branch 'main' of https://github.com/xmtp/xmtp-android into np/f…
nplasterer Dec 18, 2024
21e1ffe
all tests passing
nplasterer Dec 19, 2024
36c9ce0
fix up the lint
nplasterer Dec 19, 2024
36111bd
fix the time stamp
nplasterer Dec 19, 2024
9c3de2c
upgrade wallet connect
nplasterer Dec 19, 2024
c49ff56
remove example lint
nplasterer Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion library/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ dependencies {
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.71.0'
api 'org.xmtp:proto-kotlin:3.72.3'

testImplementation 'junit:junit:4.13.2'
testImplementation 'androidx.test:monitor:1.7.2'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.xmtp.android.library

import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.xmtp.android.library.frames.ConversationActionInputs
import org.xmtp.android.library.frames.DmActionInputs
import org.xmtp.android.library.frames.FrameActionInputs
import org.xmtp.android.library.frames.FramePostPayload
import org.xmtp.android.library.frames.FramesClient
import org.xmtp.android.library.frames.GetMetadataResponse
import java.net.HttpURLConnection
import java.net.URL

@RunWith(AndroidJUnit4::class)
class FramesTest {
@Test
fun testFramesClient() {
val frameUrl =
"https://fc-polls-five.vercel.app/polls/03710836-bc1d-4921-9e24-89d82015c53b?env=dev"
val fixtures = fixtures(ClientOptions.Api(XMTPEnvironment.DEV, isSecure = true))
val framesClient = FramesClient(xmtpClient = fixtures.alixClient)
val conversationTopic = "foo"
val participantAccountAddresses = listOf("alix", "bo")
val metadata: GetMetadataResponse
runBlocking {
metadata = framesClient.proxy.readMetadata(url = frameUrl)
}

val dmInputs = DmActionInputs(
conversationTopic = conversationTopic,
participantAccountAddresses = participantAccountAddresses
)
val conversationInputs = ConversationActionInputs.Dm(dmInputs)
val frameInputs = FrameActionInputs(
frameUrl = frameUrl,
buttonIndex = 1,
inputText = null,
state = null,
conversationInputs = conversationInputs
)
val signedPayload: FramePostPayload
runBlocking {
signedPayload = framesClient.signFrameAction(inputs = frameInputs)
}
val postUrl = metadata.extractedTags["fc:frame:post_url"]
assertNotNull(postUrl)
val response: GetMetadataResponse
runBlocking {
response = framesClient.proxy.post(url = postUrl!!, payload = signedPayload)
}

assertEquals(response.extractedTags["fc:frame"], "vNext")

val imageUrl = response.extractedTags["fc:frame:image"]
assertNotNull(imageUrl)

val mediaUrl = framesClient.proxy.mediaUrl(url = imageUrl!!)

val url = URL(mediaUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
val responseCode = connection.responseCode
assertEquals(responseCode, 200)
assertEquals(connection.contentType, "image/png")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,11 @@ class FakeSCWWallet : SigningKey {
}
}

class Fixtures {
class Fixtures(api: ClientOptions.Api = ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false)) {
val key = SecureRandom().generateSeed(32)
val context = InstrumentationRegistry.getInstrumentation().targetContext
val clientOptions = ClientOptions(
ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false),
api,
dbEncryptionKey = key,
appContext = context,
)
Expand All @@ -167,5 +167,5 @@ class Fixtures {
runBlocking { Client().create(account = caroAccount, options = clientOptions) }
}

fun fixtures(): Fixtures =
Fixtures()
fun fixtures(api: ClientOptions.Api = ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false)): Fixtures =
Fixtures(api)
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.xmtp.android.library.frames

import android.util.Base64
import com.google.protobuf.kotlin.toByteString
import org.xmtp.android.library.Client
import org.xmtp.android.library.XMTPException
import org.xmtp.android.library.frames.FramesConstants.PROTOCOL_VERSION
import org.xmtp.android.library.hexToByteArray
import org.xmtp.android.library.toHex
import org.xmtp.proto.message.contents.Frames.FrameAction
import org.xmtp.proto.message.contents.Frames.FrameActionBody
import java.security.MessageDigest
import java.util.Date

class FramesClient(private val xmtpClient: Client, var proxy: OpenFramesProxy = OpenFramesProxy()) {

suspend fun signFrameAction(inputs: FrameActionInputs): FramePostPayload {
val opaqueConversationIdentifier = buildOpaqueIdentifier(inputs)
val frameUrl = inputs.frameUrl
val buttonIndex = inputs.buttonIndex
val inputText = inputs.inputText
val state = inputs.state
val now = Date().time / 1_000
val frameActionBuilder = FrameActionBody.newBuilder().also { frame ->
frame.frameUrl = frameUrl
frame.buttonIndex = buttonIndex
frame.opaqueConversationIdentifier = opaqueConversationIdentifier
frame.unixTimestamp = now.toInt()
if (inputText != null) {
frame.inputText = inputText
}
if (state != null) {
frame.state = state
}
}

val toSign = frameActionBuilder.build()
val signedAction = Base64.encodeToString(buildSignedFrameAction(toSign), Base64.NO_WRAP)

val untrustedData = FramePostUntrustedData(
frameUrl,
now,
buttonIndex,
inputText,
state,
xmtpClient.address,
opaqueConversationIdentifier,
now.toInt()
)
val trustedData = FramePostTrustedData(signedAction)

return FramePostPayload("xmtp@$PROTOCOL_VERSION", untrustedData, trustedData)
}

private fun signDigest(message: String): ByteArray {
return xmtpClient.signWithInstallationKey(message)
}

private fun buildSignedFrameAction(actionBodyInputs: FrameActionBody): ByteArray {
val digest = sha256(actionBodyInputs.toByteArray()).toHex()
val signature = signDigest(digest)

val frameAction = FrameAction.newBuilder().also {
it.actionBody = actionBodyInputs.toByteString()
it.installationSignature = signature.toByteString()
it.installationId = xmtpClient.installationId.hexToByteArray().toByteString()
it.inboxId = xmtpClient.inboxId
}.build()

return frameAction.toByteArray()
}

private fun buildOpaqueIdentifier(inputs: FrameActionInputs): String {
return when (inputs.conversationInputs) {
is ConversationActionInputs.Group -> {
val groupInputs = inputs.conversationInputs.inputs
val combined = groupInputs.groupId + groupInputs.groupSecret
val digest = sha256(combined)
Base64.encodeToString(digest, Base64.NO_WRAP)
}

is ConversationActionInputs.Dm -> {
val dmInputs = inputs.conversationInputs.inputs
val conversationTopic =
dmInputs.conversationTopic ?: throw XMTPException("No conversation topic")
val combined =
conversationTopic.lowercase() + dmInputs.participantAccountAddresses.map { it.lowercase() }
.sorted().joinToString("")
val digest = sha256(combined.toByteArray())
Base64.encodeToString(digest, Base64.NO_WRAP)
}
}
}

private fun sha256(input: ByteArray): ByteArray {
val digest = MessageDigest.getInstance("SHA-256")
return digest.digest(input)
}
}
Loading