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

Signed frame action payload generation #168

Merged
merged 5 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"mode": "pre",
"mode": "exit",
"tag": "beta",
"initialVersions": {
"@xmtp/react-app": "0.0.0",
Expand Down
5 changes: 5 additions & 0 deletions .changeset/smooth-coats-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@xmtp/frames-client": minor
---

Add support for preparing signed payloads for the Frames API
5 changes: 4 additions & 1 deletion packages/frames-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@
"typedoc": "typedoc"
},
"peerDependencies": {
"@xmtp/xmtp-js": "^11.3.7"
"@xmtp/xmtp-js": ">9.3.1"
},
"dependencies": {
"@xmtp/proto": "3.41.0-beta.2"
}
}
43 changes: 43 additions & 0 deletions packages/frames-client/src/converters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { publicKey } from "@xmtp/proto";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to avoid actually importing xmtp-js, so I just re-implemented some functionality for converting V1 bundles to V2 bundles here.


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),
};
}
4 changes: 4 additions & 0 deletions packages/frames-client/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export async function sha256(data: Uint8Array): Promise<Uint8Array> {
const digest = await crypto.subtle.digest("SHA-256", data);
return new Uint8Array(digest);
}
68 changes: 65 additions & 3 deletions packages/frames-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Client } from "@xmtp/xmtp-js";
import { frames, fetcher } from "@xmtp/proto";
import { OG_PROXY_URL } from "./constants";
import type { FramesResponse } from "./types";
import type { FramePostPayload, FramesApiResponse } from "./types";
import { sha256 } from "./crypto";
import { v1ToV2Bundle } from "./converters";

const { b64Encode } = fetcher;

export class FramesClient {
xmtpClient: Client;
Expand All @@ -9,10 +14,67 @@ export class FramesClient {
this.xmtpClient = xmtpClient;
}

static async readMetadata(url: string): Promise<FramesResponse> {
static async readMetadata(url: string): Promise<FramesApiResponse> {
const response = await fetch(
`${OG_PROXY_URL}?url=${encodeURIComponent(url)}`,
);
return (await response.json()) as FramesResponse;
return (await response.json()) as FramesApiResponse;
}

async signFrameAction(
frameUrl: string,
buttonIndex: number,
conversationIdentifier: string,
messageId: string,
): Promise<FramePostPayload> {
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();
}
}
20 changes: 19 additions & 1 deletion packages/frames-client/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
export type FramesResponse = {
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;
};
15 changes: 14 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4304,6 +4304,7 @@ __metadata:
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:*"
Expand All @@ -4318,10 +4319,22 @@ __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

"@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

"@xmtp/proto@npm:^3.29.0":
version: 3.29.0
resolution: "@xmtp/proto@npm:3.29.0"
Expand Down