Skip to content

Commit

Permalink
Merge pull request #169 from xmtp/nm/add-frames-tests
Browse files Browse the repository at this point in the history
Ability to POST to a frame
  • Loading branch information
neekolas authored Jan 30, 2024
2 parents f67b93c + eef98fe commit 5c42717
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/four-jars-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@xmtp/frames-client": patch
---

Adds ability to post frame to a destination and see an updated response
16 changes: 16 additions & 0 deletions packages/frames-client/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
# frames-client

## Usage

```ts
const xmtpClient = await Client.create(wallet);
const framesClient = new FramesClient(xmtpClient);

const frameUrl = "https://www.myframe.xyz";

// Read data from a frame
const frameMetadata = await readMetadata(frameUrl);

// Handle a click to button 2 from a conversation with topic "/xmtp/0/123" on messageId "45678"
const payload = await signFrameAction(frameUrl, 2, "/xmtp/0/123", "45678");
const updatedFrameMetadata = await postToFrame(frameUrl, payload);
```
2 changes: 2 additions & 0 deletions packages/frames-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@xmtp/tsconfig": "workspace:*",
"@xmtp/xmtp-js": "^11.3.7",
"eslint": "^8.56.0",
"eslint-config-xmtp-web": "workspace:*",
"ethers": "^6.10.0",
"prettier": "^3.2.4",
"rollup": "^4.9.6",
"rollup-plugin-dts": "^6.1.0",
Expand Down
17 changes: 17 additions & 0 deletions packages/frames-client/src/converters.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Wallet } from "ethers";
import { it, expect, describe } from "vitest";
import { PrivateKeyBundleV1, SignedPublicKeyBundle } from "@xmtp/xmtp-js";
import { v1ToV2Bundle } from "./converters";

describe("converters", () => {
it("can convert a valid public key bundle", async () => {
const v1Bundle = await PrivateKeyBundleV1.generate(Wallet.createRandom());
const publicKeyBundle = v1Bundle.getPublicKeyBundle();

const v2Bundle = v1ToV2Bundle(publicKeyBundle);
const v2BundleInstance = new SignedPublicKeyBundle(v2Bundle);
const downgradedBundle = v2BundleInstance.toLegacyBundle();

expect(downgradedBundle.equals(publicKeyBundle)).toBe(true);
});
});
7 changes: 6 additions & 1 deletion packages/frames-client/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
// eslint-disable-next-line
const webCrypto: Crypto =
// eslint-disable-next-line
typeof crypto === "undefined" ? require("crypto").webcrypto : crypto;

export async function sha256(data: Uint8Array): Promise<Uint8Array> {
const digest = await crypto.subtle.digest("SHA-256", data);
const digest = await webCrypto.subtle.digest("SHA-256", data);
return new Uint8Array(digest);
}
82 changes: 82 additions & 0 deletions packages/frames-client/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Client, Signature, SignedPublicKey } from "@xmtp/xmtp-js";
import { Wallet } from "ethers";
import { frames, fetcher } from "@xmtp/proto";
import { it, expect, describe, beforeEach } from "vitest";
import { FramesClient } from ".";
import { sha256 } from "./crypto";

const { b64Decode } = fetcher;

describe("signFrameAction", () => {
let client: Client;
let framesClient: FramesClient;
beforeEach(async () => {
client = await Client.create(Wallet.createRandom());
framesClient = new FramesClient(client);
});
it("should sign a frame action with a valid signature", async () => {
const frameUrl = "https://example.com";
const buttonIndex = 1;
const conversationIdentifier = "testConversationIdentifier";
const messageId = "testMessageId";

const signedPayload = await framesClient.signFrameAction(
frameUrl,
buttonIndex,
conversationIdentifier,
messageId,
);

expect(signedPayload.untrustedData.walletAddress).toEqual(client.address);
expect(signedPayload.untrustedData.url).toEqual(frameUrl);
expect(signedPayload.untrustedData.buttonIndex).toEqual(buttonIndex);
expect(signedPayload.untrustedData.conversationIdentifier).toEqual(
conversationIdentifier,
);
expect(signedPayload.untrustedData.timestamp).toBeGreaterThan(0);

const signedPayloadProto = frames.FrameAction.decode(
b64Decode(signedPayload.trustedData.messageBytes),
);
expect(signedPayloadProto.actionBody).toBeDefined();
expect(signedPayloadProto.signature).toBeDefined();
expect(signedPayloadProto.signedPublicKeyBundle).toBeDefined();

const signedPayloadBody = frames.FrameActionBody.decode(
signedPayloadProto.actionBody,
);
expect(signedPayloadBody.messageId).toEqual(messageId);
expect(new TextDecoder().decode(signedPayloadBody.buttonIndex)).toEqual(
buttonIndex.toString(),
);
expect(new TextDecoder().decode(signedPayloadBody.frameUrl)).toEqual(
frameUrl,
);
expect(signedPayloadBody.conversationIdentifier).toEqual(
conversationIdentifier,
);
expect(new TextDecoder().decode(signedPayloadBody.frameUrl)).toEqual(
frameUrl,
);

if (
!signedPayloadProto.signature ||
!signedPayloadProto?.signedPublicKeyBundle?.identityKey
) {
throw new Error("Missing signature");
}

const signatureInstance = new Signature(signedPayloadProto.signature);
const digest = await sha256(signedPayloadProto.actionBody);
// Ensure the signature is valid
expect(
signatureInstance
.getPublicKey(digest)
?.equals(
new SignedPublicKey(
signedPayloadProto.signedPublicKeyBundle.identityKey,
).toLegacyKey(),
),
);
});
});
24 changes: 24 additions & 0 deletions packages/frames-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ export class FramesClient {
return (await response.json()) as FramesApiResponse;
}

static async postToFrame(
url: string,
payload: FramePostPayload,
): Promise<FramesApiResponse> {
const response = await fetch(
`${OG_PROXY_URL}?url=${encodeURIComponent(url)}`,
{
method: "POST",
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
},
},
);

if (!response.ok) {
throw new Error(
`Failed to post to frame: ${response.status} ${response.statusText}`,
);
}

return (await response.json()) as FramesApiResponse;
}

async signFrameAction(
frameUrl: string,
buttonIndex: number,
Expand Down
53 changes: 53 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3297,6 +3297,13 @@ __metadata:
languageName: node
linkType: hard

"@types/node@npm:18.15.13":
version: 18.15.13
resolution: "@types/node@npm:18.15.13"
checksum: b9bbe923573797ef7c5fd2641a6793489e25d9369c32aeadcaa5c7c175c85b42eb12d6fe173f6781ab6f42eaa1ebd9576a419eeaa2a1ec810094adb8adaa9a54
languageName: node
linkType: hard

"@types/node@npm:^12.12.54, @types/node@npm:^12.7.1":
version: 12.20.55
resolution: "@types/node@npm:12.20.55"
Expand Down Expand Up @@ -4306,8 +4313,10 @@ __metadata:
"@rollup/plugin-typescript": "npm:^11.1.6"
"@xmtp/proto": "npm:3.41.0-beta.2"
"@xmtp/tsconfig": "workspace:*"
"@xmtp/xmtp-js": "npm:^11.3.7"
eslint: "npm:^8.56.0"
eslint-config-xmtp-web: "workspace:*"
ethers: "npm:^6.10.0"
prettier: "npm:^3.2.4"
rollup: "npm:^4.9.6"
rollup-plugin-dts: "npm:^6.1.0"
Expand Down Expand Up @@ -4579,6 +4588,13 @@ __metadata:
languageName: node
linkType: hard

"aes-js@npm:4.0.0-beta.5":
version: 4.0.0-beta.5
resolution: "aes-js@npm:4.0.0-beta.5"
checksum: 8f745da2e8fb38e91297a8ec13c2febe3219f8383303cd4ed4660ca67190242ccfd5fdc2f0d1642fd1ea934818fb871cd4cc28d3f28e812e3dc6c3d0f1f97c24
languageName: node
linkType: hard

"aes-js@npm:^3.1.2":
version: 3.1.2
resolution: "aes-js@npm:3.1.2"
Expand Down Expand Up @@ -7198,6 +7214,21 @@ __metadata:
languageName: node
linkType: hard

"ethers@npm:^6.10.0":
version: 6.10.0
resolution: "ethers@npm:6.10.0"
dependencies:
"@adraffy/ens-normalize": "npm:1.10.0"
"@noble/curves": "npm:1.2.0"
"@noble/hashes": "npm:1.3.2"
"@types/node": "npm:18.15.13"
aes-js: "npm:4.0.0-beta.5"
tslib: "npm:2.4.0"
ws: "npm:8.5.0"
checksum: 04fdd3f76ea93a8b45b2fe4d9c8e2bd0d688823faba672897dd19cc3303c202a166902fe6058004562f13aaecf9f77a9f70ff113f995e94107efef2457b016dd
languageName: node
linkType: hard

"eventemitter3@npm:^4.0.7":
version: 4.0.7
resolution: "eventemitter3@npm:4.0.7"
Expand Down Expand Up @@ -12833,6 +12864,13 @@ __metadata:
languageName: node
linkType: hard

"tslib@npm:2.4.0":
version: 2.4.0
resolution: "tslib@npm:2.4.0"
checksum: d8379e68b36caf082c1905ec25d17df8261e1d68ddc1abfd6c91158a064f6e4402039ae7c02cf4c81d12e3a2a2c7cd8ea2f57b233eb80136a2e3e7279daf2911
languageName: node
linkType: hard

"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0":
version: 2.5.0
resolution: "tslib@npm:2.5.0"
Expand Down Expand Up @@ -13970,6 +14008,21 @@ __metadata:
languageName: node
linkType: hard

"ws@npm:8.5.0":
version: 8.5.0
resolution: "ws@npm:8.5.0"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: f0ee700970a0bf925b1ec213ca3691e84fb8b435a91461fe3caf52f58c6cec57c99ed5890fbf6978824c932641932019aafc55d864cad38ac32577496efd5d3a
languageName: node
linkType: hard

"ws@npm:^7.4.5, ws@npm:^7.5.1":
version: 7.5.9
resolution: "ws@npm:7.5.9"
Expand Down

0 comments on commit 5c42717

Please sign in to comment.