Skip to content
This repository has been archived by the owner on Jan 16, 2025. It is now read-only.

Commit

Permalink
Merge pull request #147 from xmtp/nm/open-frames-support
Browse files Browse the repository at this point in the history
Add OpenFrames support
  • Loading branch information
neekolas authored Feb 14, 2024
2 parents caea1fa + af6ba9f commit d6d3212
Show file tree
Hide file tree
Showing 12 changed files with 464 additions and 129 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-donkeys-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@xmtp/frames-validator": minor
---

Adds support for an Open Frames validator
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Install Yarn v3
run: |
corepack enable
corepack prepare [email protected].0 --activate
corepack prepare [email protected].2 --activate
- run: yarn
- run: yarn build
- run: yarn lint
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Install Yarn v3
run: |
corepack enable
corepack prepare [email protected].0 --activate
corepack prepare [email protected].2 --activate
- name: Install dependencies
run: yarn
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Install Yarn v4
run: |
corepack enable
corepack prepare [email protected].0 --activate
corepack prepare [email protected].2 --activate
- run: yarn
- run: yarn build
- run: yarn workspace @xmtp/bot-kit-pro run up
Expand Down
10 changes: 7 additions & 3 deletions packages/frames-validator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
"provenance": true
},
"dependencies": {
"@xmtp/proto": "3.41.0-beta.5",
"@xmtp/xmtp-js": "^11.3.5"
"@noble/hashes": "^1.3.3",
"@noble/secp256k1": "^2.0.0",
"@xmtp/proto": "3.41.0-beta.6",
"viem": "^2.7.8"
},
"scripts": {
"clean": "rm -rf dist",
Expand All @@ -41,8 +43,10 @@
"homepage": "https://github.com/xmtp/xmtp-node-js-tools#readme",
"packageManager": "[email protected]",
"devDependencies": {
"@open-frames/types": "^0.0.6",
"@rollup/plugin-typescript": "^11.1.6",
"@xmtp/frames-client": "^0.2.0",
"@xmtp/frames-client": "^0.2.2",
"@xmtp/xmtp-js": "^11.3.5",
"ethers": "^6.10.0",
"rollup": "^4.9.6",
"rollup-plugin-dts": "^6.1.0",
Expand Down
6 changes: 0 additions & 6 deletions packages/frames-validator/src/crypto.ts

This file was deleted.

102 changes: 2 additions & 100 deletions packages/frames-validator/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,100 +1,2 @@
import { fetcher, frames } from "@xmtp/proto"
import { Signature, SignedPublicKeyBundle } from "@xmtp/xmtp-js"

import { sha256 } from "./crypto.js"
import { FramePostPayload, FramePostUntrustedData } from "./types.js"
export * from "./types.js"

const { b64Decode } = fetcher

export async function validateFramesPost(data: FramePostPayload) {
const { untrustedData, trustedData } = data
const { walletAddress } = untrustedData
const { messageBytes: messageBytesString } = trustedData

const messageBytes = b64Decode(messageBytesString)

const { actionBody, actionBodyBytes, signature, signedPublicKeyBundle } =
deserializeProtoMessage(messageBytes)

const verifiedWalletAddress = await getVerifiedWalletAddress(
actionBodyBytes,
signature,
signedPublicKeyBundle,
)

if (verifiedWalletAddress !== walletAddress) {
throw new Error("Invalid wallet address")
}

await checkUntrustedData(untrustedData, actionBody)

return {
actionBody,
verifiedWalletAddress,
}
}

export function deserializeProtoMessage(messageBytes: Uint8Array) {
const frameAction = frames.FrameAction.decode(messageBytes)
if (!frameAction.signature || !frameAction.signedPublicKeyBundle) {
throw new Error(
"Invalid frame action: missing signature or signed public key bundle",
)
}
const actionBody = frames.FrameActionBody.decode(frameAction.actionBody)

return {
actionBody,
actionBodyBytes: frameAction.actionBody,
signature: new Signature(frameAction.signature),
signedPublicKeyBundle: new SignedPublicKeyBundle(
frameAction.signedPublicKeyBundle,
),
}
}

async function getVerifiedWalletAddress(
actionBodyBytes: Uint8Array,
signature: Signature,
signedPublicKeyBundle: SignedPublicKeyBundle,
): Promise<string> {
const isValid = signedPublicKeyBundle.identityKey.verify(
signature,
await sha256(actionBodyBytes),
)

if (!isValid) {
throw new Error("Invalid signature")
}

return signedPublicKeyBundle.walletSignatureAddress()
}

async function checkUntrustedData(
{
url,
buttonIndex,
opaqueConversationIdentifier,
timestamp,
}: FramePostUntrustedData,
actionBody: frames.FrameActionBody,
) {
if (actionBody.frameUrl !== url) {
throw new Error("Mismatched URL")
}

if (actionBody.buttonIndex !== buttonIndex) {
throw new Error("Mismatched button index")
}

if (
actionBody.opaqueConversationIdentifier !== opaqueConversationIdentifier
) {
throw new Error("Mismatched conversation identifier")
}

if (actionBody.timestamp.toNumber() !== timestamp) {
throw new Error("Mismatched timestamp")
}
}
export * from "./openFrames.js"
export * from "./validation.js"
57 changes: 57 additions & 0 deletions packages/frames-validator/src/openFrames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
OpenFramesRequest,
RequestValidator,
ValidationResponse,
} from "@open-frames/types"

import { XmtpOpenFramesRequest, XmtpValidationResponse } from "./types"
import { validateFramesPost } from "./validation"

export class XmtpValidator
implements
RequestValidator<XmtpOpenFramesRequest, XmtpValidationResponse, "xmtp">
{
readonly protocolIdentifier = "xmtp"
readonly minProtocolVersionDate = "2024-02-09"

minProtocolVersion(): string {
return `${this.protocolIdentifier}@${this.minProtocolVersionDate}`
}

isSupported(payload: OpenFramesRequest): payload is XmtpOpenFramesRequest {
if (!payload.clientProtocol) {
return false
}

const [protocol, version] = payload.clientProtocol.split("@")
if (!protocol || !version) {
return false
}

const isCorrectClientProtocol = protocol === "xmtp"
const isCorrectVersion = version >= this.minProtocolVersionDate
const isTrustedDataValid =
typeof payload.trustedData?.messageBytes === "string"

return isCorrectClientProtocol && isCorrectVersion && isTrustedDataValid
}

async validate(
payload: XmtpOpenFramesRequest,
): Promise<
ValidationResponse<XmtpValidationResponse, typeof this.protocolIdentifier>
> {
try {
const validationResponse = await validateFramesPost(payload)
return {
isValid: true,
clientProtocol: payload.clientProtocol,
message: validationResponse,
}
} catch (error) {
return {
isValid: false,
}
}
}
}
23 changes: 14 additions & 9 deletions packages/frames-validator/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
export type FramePostUntrustedData = {
import type {
OpenFramesTrustedData,
OpenFramesUntrustedData,
} from "@open-frames/types"
import { frames } from "@xmtp/proto"

export type UntrustedData = OpenFramesUntrustedData & {
walletAddress: string // Untrusted version of the wallet address
url: string // Frame URL. May be different from the `post_url` this is being sent to
timestamp: number // Timestamp in milliseconds
buttonIndex: number // 1-indexed button that was clicked
opaqueConversationIdentifier: string // A hash of the conversation topic and the participants
}

export type FramePostTrustedData = {
messageBytes: string
export type XmtpOpenFramesRequest = {
clientProtocol: `xmtp@${string}`
untrustedData: UntrustedData
trustedData: OpenFramesTrustedData
}

export type FramePostPayload = {
untrustedData: FramePostUntrustedData
trustedData: FramePostTrustedData
export type XmtpValidationResponse = {
actionBody: frames.FrameActionBody
verifiedWalletAddress: string
}
Loading

0 comments on commit d6d3212

Please sign in to comment.