-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* try and remove the bad dependencies * getting closer on removing the cocoapods * more clean up * update dependenecies * update package resolved * remove all the old cocopods * try bumping the xcode version down * remove the old gzip library * remove force unwrap * remove another force unwrap * add back sec * add target * try with another library * another try on signing * get the example app building * bump the proto * bring back all the frames code * update the signer * update the protos * small tweak to test * closer on frames * get the tests passing
- Loading branch information
1 parent
bd7c6b0
commit cb9e270
Showing
7 changed files
with
628 additions
and
109 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// | ||
// FramesClient.swift | ||
// | ||
// | ||
// Created by Alex Risch on 3/28/24. | ||
// | ||
|
||
import Foundation | ||
import LibXMTP | ||
|
||
public typealias FrameActionBody = Xmtp_MessageContents_FrameActionBody | ||
public typealias FrameAction = Xmtp_MessageContents_FrameAction | ||
|
||
enum FramesClientError: Error { | ||
case missingConversationTopic | ||
case missingTarget | ||
case readMetadataFailed(message: String, code: Int) | ||
case postFrameFailed(message: String, code: Int) | ||
} | ||
|
||
public class FramesClient { | ||
var xmtpClient: Client | ||
public var proxy: OpenFramesProxy | ||
|
||
public init(xmtpClient: Client, proxy: OpenFramesProxy? = nil) { | ||
self.xmtpClient = xmtpClient | ||
self.proxy = proxy ?? OpenFramesProxy() | ||
} | ||
|
||
public func signFrameAction(inputs: FrameActionInputs) async throws | ||
-> FramePostPayload | ||
{ | ||
let opaqueConversationIdentifier = try self.buildOpaqueIdentifier( | ||
inputs: inputs) | ||
let frameUrl = inputs.frameUrl | ||
let buttonIndex = inputs.buttonIndex | ||
let inputText = inputs.inputText ?? "" | ||
let state = inputs.state ?? "" | ||
let now = Date().timeIntervalSince1970 | ||
let timestamp = now | ||
|
||
var toSign = FrameActionBody() | ||
toSign.frameURL = frameUrl | ||
toSign.buttonIndex = buttonIndex | ||
toSign.opaqueConversationIdentifier = opaqueConversationIdentifier | ||
toSign.timestamp = UInt64(timestamp) | ||
toSign.inputText = inputText | ||
toSign.unixTimestamp = UInt32(now) | ||
toSign.state = state | ||
|
||
let signedAction = try await self.buildSignedFrameAction( | ||
actionBodyInputs: toSign) | ||
|
||
let untrustedData = FramePostUntrustedData( | ||
url: frameUrl, timestamp: UInt64(now), buttonIndex: buttonIndex, | ||
inputText: inputText, state: state, | ||
walletAddress: xmtpClient.address, | ||
opaqueConversationIdentifier: opaqueConversationIdentifier, | ||
unixTimestamp: UInt32(now) | ||
) | ||
|
||
let trustedData = FramePostTrustedData( | ||
messageBytes: signedAction.base64EncodedString()) | ||
|
||
let payload = FramePostPayload( | ||
clientProtocol: "xmtp@\(PROTOCOL_VERSION)", | ||
untrustedData: untrustedData, trustedData: trustedData | ||
) | ||
|
||
return payload | ||
} | ||
|
||
private func signDigest(message: String) async throws -> Data { | ||
return try xmtpClient.signWithInstallationKey(message: message) | ||
} | ||
|
||
private func buildSignedFrameAction(actionBodyInputs: FrameActionBody) | ||
async throws -> Data | ||
{ | ||
let digest = sha256(input: try actionBodyInputs.serializedData()).toHex | ||
let signature = try await self.signDigest(message: digest) | ||
|
||
var frameAction = FrameAction() | ||
frameAction.actionBody = try actionBodyInputs.serializedData() | ||
frameAction.installationSignature = signature | ||
frameAction.installationID = xmtpClient.installationID.hexToData | ||
frameAction.inboxID = xmtpClient.inboxID | ||
|
||
return try frameAction.serializedData() | ||
} | ||
|
||
private func buildOpaqueIdentifier(inputs: FrameActionInputs) throws | ||
-> String | ||
{ | ||
switch inputs.conversationInputs { | ||
case .group(let groupInputs): | ||
let combined = groupInputs.groupId + groupInputs.groupSecret | ||
let digest = sha256(input: combined) | ||
return digest.base64EncodedString() | ||
case .dm(let dmInputs): | ||
guard let conversationTopic = dmInputs.conversationTopic else { | ||
throw FramesClientError.missingConversationTopic | ||
} | ||
guard | ||
let combined = | ||
(conversationTopic.lowercased() | ||
+ dmInputs.participantAccountAddresses.map { | ||
$0.lowercased() | ||
}.sorted().joined()).data(using: .utf8) | ||
else { | ||
throw FramesClientError.missingConversationTopic | ||
} | ||
let digest = sha256(input: combined) | ||
return digest.base64EncodedString() | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by Alex Risch on 3/28/24. | ||
// | ||
|
||
import Foundation | ||
|
||
let OPEN_FRAMES_PROXY_URL = "https://frames.xmtp.chat/" | ||
|
||
let PROTOCOL_VERSION = "2024-02-09" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by Alex Risch on 3/28/24. | ||
// | ||
|
||
import Foundation | ||
|
||
typealias AcceptedFrameClients = [String: String] | ||
|
||
enum OpenFrameButton: Codable { | ||
case link(target: String, label: String) | ||
case mint(target: String, label: String) | ||
case post(target: String?, label: String) | ||
case postRedirect(target: String?, label: String) | ||
|
||
enum CodingKeys: CodingKey { | ||
case action, target, label | ||
} | ||
|
||
init(from decoder: Decoder) throws { | ||
let container = try decoder.container(keyedBy: CodingKeys.self) | ||
let action = try container.decode(String.self, forKey: .action) | ||
guard let target = try container.decodeIfPresent(String.self, forKey: .target) else { | ||
throw FramesClientError.missingTarget | ||
} | ||
let label = try container.decode(String.self, forKey: .label) | ||
|
||
switch action { | ||
case "link": | ||
self = .link(target: target, label: label) | ||
case "mint": | ||
self = .mint(target: target, label: label) | ||
case "post": | ||
self = .post(target: target, label: label) | ||
case "post_redirect": | ||
self = .postRedirect(target: target, label: label) | ||
default: | ||
throw DecodingError.dataCorruptedError(forKey: .action, in: container, debugDescription: "Invalid action value") | ||
} | ||
} | ||
|
||
func encode(to encoder: Encoder) throws { | ||
var container = encoder.container(keyedBy: CodingKeys.self) | ||
switch self { | ||
case .link(let target, let label): | ||
try container.encode("link", forKey: .action) | ||
try container.encode(target, forKey: .target) | ||
try container.encode(label, forKey: .label) | ||
case .mint(let target, let label): | ||
try container.encode("mint", forKey: .action) | ||
try container.encode(target, forKey: .target) | ||
try container.encode(label, forKey: .label) | ||
case .post(let target, let label): | ||
try container.encode("post", forKey: .action) | ||
try container.encode(target, forKey: .target) | ||
try container.encode(label, forKey: .label) | ||
case .postRedirect(let target, let label): | ||
try container.encode("post_redirect", forKey: .action) | ||
try container.encode(target, forKey: .target) | ||
try container.encode(label, forKey: .label) | ||
} | ||
} | ||
} | ||
|
||
public struct OpenFrameImage: Codable { | ||
let content: String | ||
let aspectRatio: AspectRatio? | ||
let alt: String? | ||
} | ||
|
||
public enum AspectRatio: String, Codable { | ||
case ratio_1_91_1 = "1.91.1" | ||
case ratio_1_1 = "1:1" | ||
} | ||
|
||
public struct TextInput: Codable { | ||
let content: String | ||
} | ||
|
||
struct OpenFrameResult: Codable { | ||
let acceptedClients: AcceptedFrameClients | ||
let image: OpenFrameImage | ||
let postUrl: String? | ||
let textInput: TextInput? | ||
let buttons: [String: OpenFrameButton]? | ||
let ogImage: String | ||
let state: String? | ||
}; | ||
|
||
public struct GetMetadataResponse: Codable { | ||
let url: String | ||
public let extractedTags: [String: String] | ||
} | ||
|
||
public struct PostRedirectResponse: Codable { | ||
let originalUrl: String | ||
let redirectedTo: String | ||
}; | ||
|
||
public struct OpenFramesUntrustedData: Codable { | ||
let url: String | ||
let timestamp: Int | ||
let buttonIndex: Int | ||
let inputText: String? | ||
let state: String? | ||
} | ||
|
||
public typealias FramesApiRedirectResponse = PostRedirectResponse; | ||
|
||
public struct FramePostUntrustedData: Codable { | ||
let url: String | ||
let timestamp: UInt64 | ||
let buttonIndex: Int32 | ||
let inputText: String? | ||
let state: String? | ||
let walletAddress: String | ||
let opaqueConversationIdentifier: String | ||
let unixTimestamp: UInt32 | ||
} | ||
|
||
public struct FramePostTrustedData: Codable { | ||
let messageBytes: String | ||
} | ||
|
||
public struct FramePostPayload: Codable { | ||
let clientProtocol: String | ||
let untrustedData: FramePostUntrustedData | ||
let trustedData: FramePostTrustedData | ||
} | ||
|
||
public struct DmActionInputs: Codable { | ||
public let conversationTopic: String? | ||
public let participantAccountAddresses: [String] | ||
public init(conversationTopic: String? = nil, participantAccountAddresses: [String]) { | ||
self.conversationTopic = conversationTopic | ||
self.participantAccountAddresses = participantAccountAddresses | ||
} | ||
} | ||
|
||
public struct GroupActionInputs: Codable { | ||
let groupId: Data | ||
let groupSecret: Data | ||
} | ||
|
||
public enum ConversationActionInputs: Codable { | ||
case dm(DmActionInputs) | ||
case group(GroupActionInputs) | ||
} | ||
|
||
public struct FrameActionInputs: Codable { | ||
let frameUrl: String | ||
let buttonIndex: Int32 | ||
let inputText: String? | ||
let state: String? | ||
let conversationInputs: ConversationActionInputs | ||
public init(frameUrl: String, buttonIndex: Int32, inputText: String?, state: String?, conversationInputs: ConversationActionInputs) { | ||
self.frameUrl = frameUrl | ||
self.buttonIndex = buttonIndex | ||
self.inputText = inputText | ||
self.state = state | ||
self.conversationInputs = conversationInputs | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by Alex Risch on 3/28/24. | ||
// | ||
|
||
import Foundation | ||
|
||
public class OpenFramesProxy { | ||
let inner: ProxyClient | ||
|
||
init(baseUrl: String = OPEN_FRAMES_PROXY_URL) { | ||
self.inner = ProxyClient(baseUrl: baseUrl); | ||
} | ||
|
||
public func readMetadata(url: String) async throws -> GetMetadataResponse { | ||
return try await self.inner.readMetadata(url: url); | ||
} | ||
|
||
public func post(url: String, payload: FramePostPayload) async throws -> GetMetadataResponse { | ||
return try await self.inner.post(url: url, payload: payload); | ||
} | ||
|
||
public func postRedirect( | ||
url: String, | ||
payload: FramePostPayload | ||
) async throws -> FramesApiRedirectResponse { | ||
return try await self.inner.postRedirect(url: url, payload: payload); | ||
} | ||
|
||
public func mediaUrl(url: String) async throws -> String { | ||
if url.hasPrefix("data:") { | ||
return url | ||
} | ||
return self.inner.mediaUrl(url: url); | ||
} | ||
} |
Oops, something went wrong.