Skip to content

Commit

Permalink
bring back the frames signer
Browse files Browse the repository at this point in the history
  • Loading branch information
nplasterer committed Nov 6, 2024
1 parent 6e28245 commit b77670e
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 165 deletions.
142 changes: 71 additions & 71 deletions library/src/androidTest/java/org/xmtp/android/library/FramesTest.kt
Original file line number Diff line number Diff line change
@@ -1,71 +1,71 @@
//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/01032f47-e976-42ee-9e3d-3aac1324f4b8"
// val fixtures = fixtures()
// val aliceClient = fixtures.aliceClient
//
// val framesClient = FramesClient(xmtpClient = aliceClient)
// 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")
// }
//}
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/01032f47-e976-42ee-9e3d-3aac1324f4b8"
val fixtures = fixtures()
val aliceClient = fixtures.aliceClient

val framesClient = FramesClient(xmtpClient = aliceClient)
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")
}
}
184 changes: 90 additions & 94 deletions library/src/main/java/org/xmtp/android/library/frames/FramesClient.kt
Original file line number Diff line number Diff line change
@@ -1,94 +1,90 @@
//package org.xmtp.android.library.frames
//
//import android.util.Base64
//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.messages.PrivateKeyBuilder
//import org.xmtp.android.library.messages.Signature
//import org.xmtp.android.library.messages.getPublicKeyBundle
//import org.xmtp.proto.message.contents.PublicKeyOuterClass.SignedPublicKeyBundle
//import java.security.MessageDigest
//import org.xmtp.proto.message.contents.Frames.FrameActionBody
//import org.xmtp.proto.message.contents.Frames.FrameAction
//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_000
// val frameActionBuilder = FrameActionBody.newBuilder().also { frame ->
// frame.frameUrl = frameUrl
// frame.buttonIndex = buttonIndex
// frame.opaqueConversationIdentifier = opaqueConversationIdentifier
// frame.timestamp = now
// 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 suspend fun signDigest(digest: ByteArray): Signature {
// val signedPrivateKey = xmtpClient.keys.identityKey
// val privateKey = PrivateKeyBuilder.buildFromSignedPrivateKey(signedPrivateKey)
// return PrivateKeyBuilder(privateKey).sign(digest)
// }
//
// private fun getPublicKeyBundle(): SignedPublicKeyBundle {
// return xmtpClient.keys.getPublicKeyBundle()
// }
//
// private suspend fun buildSignedFrameAction(actionBodyInputs: FrameActionBody): ByteArray {
// val digest = sha256(actionBodyInputs.toByteArray())
// val signature = signDigest(digest)
//
// val publicKeyBundle = getPublicKeyBundle()
// val frameAction = FrameAction.newBuilder().also {
// it.actionBody = actionBodyInputs.toByteString()
// it.signature = signature
// it.signedPublicKeyBundle = publicKeyBundle
// }.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("")).toByteArray()
// val digest = sha256(combined)
// Base64.encodeToString(digest, Base64.NO_WRAP)
// }
// }
// }
//
// private fun sha256(input: ByteArray): ByteArray {
// val digest = MessageDigest.getInstance("SHA-256")
// return digest.digest(input)
// }
//}
package org.xmtp.android.library.frames

import android.util.Base64
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.messages.PrivateKeyBuilder
import org.xmtp.android.library.messages.Signature
import org.xmtp.android.library.messages.getPublicKeyBundle
import org.xmtp.proto.message.contents.PublicKeyOuterClass.SignedPublicKeyBundle
import java.security.MessageDigest
import org.xmtp.proto.message.contents.Frames.FrameActionBody
import org.xmtp.proto.message.contents.Frames.FrameAction
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_000
val frameActionBuilder = FrameActionBody.newBuilder().also { frame ->
frame.frameUrl = frameUrl
frame.buttonIndex = buttonIndex
frame.opaqueConversationIdentifier = opaqueConversationIdentifier
frame.timestamp = now
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 suspend fun signDigest(digest: ByteArray): Signature {
val signedPrivateKey = xmtpClient.keys.identityKey
val privateKey = PrivateKeyBuilder.buildFromSignedPrivateKey(signedPrivateKey)
return PrivateKeyBuilder(privateKey).sign(digest)
}

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

val publicKeyBundle = getPublicKeyBundle()
val frameAction = FrameAction.newBuilder().also {
it.actionBody = actionBodyInputs.toByteString()
it.signature = signature
it.signedPublicKeyBundle = publicKeyBundle
}.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("")).toByteArray()
val digest = sha256(combined)
Base64.encodeToString(digest, Base64.NO_WRAP)
}
}
}

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

0 comments on commit b77670e

Please sign in to comment.