Skip to content

Commit

Permalink
Merge pull request #168 from xmtp/nm/signed-frame-action-payload
Browse files Browse the repository at this point in the history
Signed frame action payload generation
  • Loading branch information
neekolas authored Jan 30, 2024
2 parents b5ac116 + 978124c commit 67270ed
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 7 deletions.
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";

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

0 comments on commit 67270ed

Please sign in to comment.