From bcc96b2a08f2c0f0b5ac9d6f1816d41cae868599 Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:08:06 -0800 Subject: [PATCH 1/4] Add signed frame action payloads --- packages/frames-client/package.json | 95 ++++++++++++++++++++++++ packages/frames-client/src/converters.ts | 43 +++++++++++ packages/frames-client/src/crypto.ts | 4 + packages/frames-client/src/index.ts | 80 ++++++++++++++++++++ packages/frames-client/src/types.ts | 22 ++++++ yarn.lock | 40 ++++++++++ 6 files changed, 284 insertions(+) create mode 100644 packages/frames-client/package.json create mode 100644 packages/frames-client/src/converters.ts create mode 100644 packages/frames-client/src/crypto.ts create mode 100644 packages/frames-client/src/index.ts create mode 100644 packages/frames-client/src/types.ts diff --git a/packages/frames-client/package.json b/packages/frames-client/package.json new file mode 100644 index 00000000..b5a12277 --- /dev/null +++ b/packages/frames-client/package.json @@ -0,0 +1,95 @@ +{ + "name": "@xmtp/frames-client", + "packageManager": "yarn@4.0.2", + "version": "0.0.1", + "author": "XMTP Labs ", + "license": "MIT", + "type": "module", + "browser": "lib/index.js", + "module": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + } + }, + "files": [ + "lib" + ], + "sideEffects": false, + "repository": { + "type": "git", + "url": "git@github.com:xmtp/xmtp-web.git", + "directory": "packages/react-sdk" + }, + "homepage": "https://github.com/xmtp/xmtp-web", + "bugs": { + "url": "https://github.com/xmtp/xmtp-web/issues" + }, + "keywords": [ + "xmtp", + "messaging", + "web3", + "sdk", + "js", + "ts", + "javascript", + "typescript", + "react", + "reactjs", + "react-hooks", + "hooks" + ], + "publishConfig": { + "access": "public" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome versions", + "last 3 firefox versions", + "last 3 safari versions" + ] + }, + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^11.1.6", + "@xmtp/tsconfig": "workspace:*", + "eslint": "^8.56.0", + "eslint-config-xmtp-web": "workspace:*", + "prettier": "^3.2.4", + "rollup": "^4.9.6", + "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-filesize": "^10.0.0", + "rollup-plugin-tsconfig-paths": "^1.5.2", + "typedoc": "^0.25.7", + "typescript": "^5.3.3", + "vite": "^5.0.12", + "vite-tsconfig-paths": "^4.3.1", + "vitest": "^1.2.1" + }, + "scripts": { + "build": "yarn clean:lib && yarn rollup -c", + "dev": "yarn clean:lib && yarn rollup -c --watch", + "clean:lib": "rm -rf lib", + "clean": "rm -rf .turbo && rm -rf node_modules && yarn clean:lib", + "lint": "eslint . --ignore-path ../../.gitignore", + "format:base": "prettier --ignore-path ../../.gitignore", + "format:check": "yarn format:base -c .", + "format": "yarn format:base -w .", + "test": "vitest run --passWithNoTests", + "typecheck": "tsc", + "typedoc": "typedoc" + }, + "peerDependencies": { + "@xmtp/xmtp-js": ">9.3.1" + }, + "dependencies": { + "@xmtp/proto": "3.41.0-beta.2" + } +} diff --git a/packages/frames-client/src/converters.ts b/packages/frames-client/src/converters.ts new file mode 100644 index 00000000..6b267ade --- /dev/null +++ b/packages/frames-client/src/converters.ts @@ -0,0 +1,43 @@ +import { publicKey, signature } from "@xmtp/proto"; + +function publicKeyBytesToSign(pubKey: publicKey.PublicKey): Uint8Array { + return publicKey.PublicKey.encode({ + timestamp: pubKey.timestamp, + secp256k1Uncompressed: pubKey.secp256k1Uncompressed, + }).finish(); +} + +function toSignedPublicKey( + v1Key: publicKey.PublicKey, + signedByWallet: boolean, +): publicKey.SignedPublicKey { + if (!v1Key.signature) { + throw new Error("Missing signature"); + } + + let v1Signature = v1Key.signature; + if (signedByWallet) { + v1Signature = { + walletEcdsaCompact: v1Signature.ecdsaCompact, + ecdsaCompact: undefined, + }; + } + + return { + keyBytes: publicKeyBytesToSign(v1Key), + signature: v1Signature, + }; +} + +export function v1ToV2Bundle( + v1Bundle: publicKey.PublicKeyBundle, +): publicKey.SignedPublicKeyBundle { + if (!v1Bundle.identityKey || !v1Bundle.preKey) { + throw new Error("Invalid bundle"); + } + + return { + identityKey: toSignedPublicKey(v1Bundle.identityKey, true), + preKey: toSignedPublicKey(v1Bundle.preKey, false), + }; +} diff --git a/packages/frames-client/src/crypto.ts b/packages/frames-client/src/crypto.ts new file mode 100644 index 00000000..b45c2f8f --- /dev/null +++ b/packages/frames-client/src/crypto.ts @@ -0,0 +1,4 @@ +export async function sha256(data: Uint8Array): Promise { + const digest = await crypto.subtle.digest("SHA-256", data); + return new Uint8Array(digest); +} diff --git a/packages/frames-client/src/index.ts b/packages/frames-client/src/index.ts new file mode 100644 index 00000000..22b78c89 --- /dev/null +++ b/packages/frames-client/src/index.ts @@ -0,0 +1,80 @@ +import type { Client } from "@xmtp/xmtp-js"; +import { frames, fetcher } from "@xmtp/proto"; +import { OG_PROXY_URL } from "./constants"; +import type { FramePostPayload, FramesApiResponse } from "./types"; +import { sha256 } from "./crypto"; +import { v1ToV2Bundle } from "./converters"; + +const { b64Encode } = fetcher; + +export class FramesClient { + xmtpClient: Client; + + constructor(xmtpClient: Client) { + this.xmtpClient = xmtpClient; + } + + static async readMetadata(url: string): Promise { + const response = await fetch( + `${OG_PROXY_URL}?url=${encodeURIComponent(url)}`, + ); + return (await response.json()) as FramesApiResponse; + } + + async signFrameAction( + frameUrl: string, + buttonIndex: number, + conversationIdentifier: string, + messageId: string, + ): Promise { + const signedAction = await this.buildSignedFrameAction( + frameUrl, + buttonIndex, + conversationIdentifier, + messageId, + ); + + return { + untrustedData: { + walletAddress: this.xmtpClient.address, + url: frameUrl, + messageId, + timestamp: Date.now(), + buttonIndex, + conversationIdentifier, + }, + trustedData: { + messageBytes: b64Encode(signedAction, 0, signedAction.length), + }, + }; + } + + private async buildSignedFrameAction( + frameUrl: string, + buttonIndex: number, + conversationIdentifier: string, + messageId: string, + ) { + const actionBody = frames.FrameActionBody.encode({ + frameUrl: new TextEncoder().encode(frameUrl), + buttonIndex: new TextEncoder().encode(buttonIndex.toString()), + conversationIdentifier, + messageId, + }).finish(); + + const digest = await sha256(actionBody); + const signature = await this.xmtpClient.keystore.signDigest({ + digest, + identityKey: true, + prekeyIndex: undefined, + }); + + const publicKeyBundle = await this.xmtpClient.keystore.getPublicKeyBundle(); + + return frames.FrameAction.encode({ + actionBody, + signature, + signedPublicKeyBundle: v1ToV2Bundle(publicKeyBundle), + }).finish(); + } +} diff --git a/packages/frames-client/src/types.ts b/packages/frames-client/src/types.ts new file mode 100644 index 00000000..46c3b255 --- /dev/null +++ b/packages/frames-client/src/types.ts @@ -0,0 +1,22 @@ +export type FramesApiResponse = { + url: string; + extractedTags: { [k: string]: string }; +}; + +export type FramePostUntrustedData = { + walletAddress: string; + url: string; + messageId: string; + timestamp: number; + buttonIndex: number; + conversationIdentifier: string; +}; + +export type FramePostTrustedData = { + messageBytes: string; +}; + +export type FramePostPayload = { + untrustedData: FramePostUntrustedData; + trustedData: FramePostTrustedData; +}; diff --git a/yarn.lock b/yarn.lock index d71379bd..ba76638e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4298,6 +4298,46 @@ __metadata: languageName: node linkType: hard +<<<<<<< Updated upstream +======= +"@xmtp/frames-client@workspace:packages/frames-client": + version: 0.0.0-use.local + resolution: "@xmtp/frames-client@workspace:packages/frames-client" + dependencies: + "@rollup/plugin-terser": "npm:^0.4.4" + "@rollup/plugin-typescript": "npm:^11.1.6" + "@xmtp/proto": "npm:3.41.0-beta.2" + "@xmtp/tsconfig": "workspace:*" + eslint: "npm:^8.56.0" + eslint-config-xmtp-web: "workspace:*" + prettier: "npm:^3.2.4" + rollup: "npm:^4.9.6" + rollup-plugin-dts: "npm:^6.1.0" + rollup-plugin-filesize: "npm:^10.0.0" + rollup-plugin-tsconfig-paths: "npm:^1.5.2" + typedoc: "npm:^0.25.7" + typescript: "npm:^5.3.3" + vite: "npm:^5.0.12" + vite-tsconfig-paths: "npm:^4.3.1" + vitest: "npm:^1.2.1" + peerDependencies: + "@xmtp/xmtp-js": ^11.3.7 + languageName: unknown + linkType: soft + +"@xmtp/proto@npm:3.41.0-beta.2": + version: 3.41.0-beta.2 + resolution: "@xmtp/proto@npm:3.41.0-beta.2" + dependencies: + long: "npm:^5.2.0" + protobufjs: "npm:^7.0.0" + rxjs: "npm:^7.8.0" + undici: "npm:^5.8.1" + checksum: 494d99f3346bc56e7b80133f7131d86a620c24aff81107288d9eae848f43a0b87071f805ac494ff48c32beb81559a078dabd99e21a2a9a54c2c83fab8d10523b + languageName: node + linkType: hard + +>>>>>>> Stashed changes "@xmtp/proto@npm:^3.29.0": version: 3.29.0 resolution: "@xmtp/proto@npm:3.29.0" From fc9fbea658a5d60bf08cde9982b371ab6fdac4f1 Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:12:35 -0800 Subject: [PATCH 2/4] Fix merge conflicts --- yarn.lock | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index ba76638e..1a1ca439 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4298,8 +4298,6 @@ __metadata: languageName: node linkType: hard -<<<<<<< Updated upstream -======= "@xmtp/frames-client@workspace:packages/frames-client": version: 0.0.0-use.local resolution: "@xmtp/frames-client@workspace:packages/frames-client" @@ -4321,7 +4319,7 @@ __metadata: vite-tsconfig-paths: "npm:^4.3.1" vitest: "npm:^1.2.1" peerDependencies: - "@xmtp/xmtp-js": ^11.3.7 + "@xmtp/xmtp-js": ">9.3.1" languageName: unknown linkType: soft @@ -4337,7 +4335,6 @@ __metadata: languageName: node linkType: hard ->>>>>>> Stashed changes "@xmtp/proto@npm:^3.29.0": version: 3.29.0 resolution: "@xmtp/proto@npm:3.29.0" From a04afacb45bb320add6908f762c15b56ad66ac5b Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:15:16 -0800 Subject: [PATCH 3/4] Add changeset --- .changeset/pre.json | 2 +- .changeset/smooth-coats-glow.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/smooth-coats-glow.md diff --git a/.changeset/pre.json b/.changeset/pre.json index caed0030..031abb51 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "beta", "initialVersions": { "@xmtp/react-app": "0.0.0", diff --git a/.changeset/smooth-coats-glow.md b/.changeset/smooth-coats-glow.md new file mode 100644 index 00000000..bd50e42e --- /dev/null +++ b/.changeset/smooth-coats-glow.md @@ -0,0 +1,5 @@ +--- +"@xmtp/frames-client": minor +--- + +Add support for preparing signed payloads for the Frames API From 978124cb83878545d127df24512ee2bff17b2697 Mon Sep 17 00:00:00 2001 From: Nicholas Molnar <65710+neekolas@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:15:55 -0800 Subject: [PATCH 4/4] Lint --- packages/frames-client/src/converters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frames-client/src/converters.ts b/packages/frames-client/src/converters.ts index 6b267ade..5266dae5 100644 --- a/packages/frames-client/src/converters.ts +++ b/packages/frames-client/src/converters.ts @@ -1,4 +1,4 @@ -import { publicKey, signature } from "@xmtp/proto"; +import { publicKey } from "@xmtp/proto"; function publicKeyBytesToSign(pubKey: publicKey.PublicKey): Uint8Array { return publicKey.PublicKey.encode({