diff --git a/bun.lockb b/bun.lockb index d26d007e..d1623063 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/example/src/index.tsx b/example/src/index.tsx index feb34b36..1245e903 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -2,43 +2,44 @@ /** @jsxImportSource hono/jsx */ /** @jsxFrag */ -import { Button, Framework } from "@wevm/framework"; +import { Button, Framework } from '@wevm/framework' -const app = new Framework(); +const app = new Framework() -app.frame("/", ({ untrustedData }) => { - const { buttonIndex } = untrustedData || {}; +app.frame('/', ({ untrustedData }) => { + const { buttonIndex } = untrustedData || {} + console.log('buttonIndex', buttonIndex) return { image: (
- {typeof buttonIndex === "number" + {typeof buttonIndex === 'number' ? `Button Index: ${buttonIndex}` - : "Welcome!"} + : 'Welcome!'}
), @@ -46,12 +47,15 @@ app.frame("/", ({ untrustedData }) => { <> + ), - }; -}); + } +}) export default { port: 3001, fetch: app.fetch, -}; +} diff --git a/src/index.tsx b/src/index.tsx index d9e12389..6d99e34d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,14 @@ import { ImageResponse } from 'hono-og' import { type JSXNode } from 'hono/jsx' import { Window } from 'happy-dom' import { jsxRenderer } from 'hono/jsx-renderer' +import * as ed from '@noble/ed25519' +import { bytesToHex } from 'viem/utils' +import { + NobleEd25519Signer, + makeFrameAction, + Message, + FrameActionBody, +} from '@farcaster/core' import { type Frame, @@ -30,34 +38,148 @@ type FrameReturnType = { intents: JSX.Element } +const renderer = jsxRenderer( + ({ children }) => { + return ( + + {children} + + ) + }, + { docType: true }, +) + export class Framework extends Hono { frame( path: string, handler: (c: FrameContext) => FrameReturnType | Promise, ) { - this.get( - '/preview', - jsxRenderer( - ({ children }) => { - return ( - - {children} - - ) - }, - { docType: true }, - ), - ) + this.get('/preview', renderer) + this.post('/preview', renderer) this.get('/preview/*', async (c) => { const baseUrl = c.req.url.replace('/preview', '') const response = await fetch(baseUrl) - const html = await response.text() - const frame = htmlToFrame(html) + const text = await response.text() + const frame = htmlToFrame(text) return c.render( -
- Farcaster frame -
, + <> + + , + ) + }) + + this.post('/preview', async (c) => { + const baseUrl = c.req.url.replace('/preview', '') + + const formData = await c.req.formData() + const buttonIndex = parseInt( + typeof formData.get('buttonIndex') === 'string' + ? (formData.get('buttonIndex') as string) + : '', + ) + const inputText = formData.get('inputText') + ? Buffer.from(formData.get('inputText') as string) + : undefined + + const privateKeyBytes = ed.utils.randomPrivateKey() + // const publicKeyBytes = await ed.getPublicKeyAsync(privateKeyBytes) + + // const key = bytesToHex(publicKeyBytes) + // const deadline = Math.floor(Date.now() / 1000) + 60 * 60 // now + hour + // + // const account = privateKeyToAccount(bytesToHex(privateKeyBytes)) + // const requestFid = 1 + + // const signature = await account.signTypedData({ + // domain: { + // name: 'Farcaster SignedKeyRequestValidator', + // version: '1', + // chainId: 10, + // verifyingContract: '0x00000000FC700472606ED4fA22623Acf62c60553', + // }, + // types: { + // SignedKeyRequest: [ + // { name: 'requestFid', type: 'uint256' }, + // { name: 'key', type: 'bytes' }, + // { name: 'deadline', type: 'uint256' }, + // ], + // }, + // primaryType: 'SignedKeyRequest', + // message: { + // requestFid: BigInt(requestFid), + // key, + // deadline: BigInt(deadline), + // }, + // }) + + // const response = await fetch( + // 'https://api.warpcast.com/v2/signed-key-requests', + // { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // deadline, + // key, + // requestFid, + // signature, + // }), + // }, + // ) + + const fid = 2 + const castId = { + fid, + hash: new Uint8Array( + Buffer.from('0000000000000000000000000000000000000000', 'hex'), + ), + } + const frameActionBody = FrameActionBody.create({ + url: Buffer.from(baseUrl), + buttonIndex, + castId, + inputText, + }) + const frameActionMessage = await makeFrameAction( + frameActionBody, + { fid, network: 1 }, + new NobleEd25519Signer(privateKeyBytes), + ) + + const message = frameActionMessage._unsafeUnwrap() + const response = await fetch(baseUrl, { + method: 'POST', + body: JSON.stringify({ + untrustedData: { + buttonIndex, + castId: { + fid: castId.fid, + hash: bytesToHex(castId.hash), + }, + fid, + inputText, + messageHash: bytesToHex(message.hash), + network: 1, + timestamp: message.data.timestamp, + url: baseUrl, + }, + trustedData: { + messageBytes: Buffer.from( + Message.encode(message).finish(), + ).toString('hex'), + }, + }), + }) + const text = await response.text() + // TODO: handle redirects + const frame = htmlToFrame(text) + + return c.render( + <> + + , ) }) @@ -103,6 +225,40 @@ export class Framework extends Hono { //////////////////////////////////////////////////////////////////////// // Components +type FramePreviewProps = { + baseUrl: string + frame: Frame +} + +function FramePreview({ baseUrl, frame }: FramePreviewProps) { + return ( +
+
+ +
+ {frame.title +
{new URL(baseUrl).host}
+
+ {/* TODO: Text input */} + {frame.buttons && ( +
+ {frame.buttons.map((button) => ( + + ))} +
+ )} +
+
+ ) +} + export type ButtonProps = { children: string } diff --git a/src/package.json b/src/package.json index c77d0c0e..57c140cc 100644 --- a/src/package.json +++ b/src/package.json @@ -13,7 +13,10 @@ "hono": "^3" }, "dependencies": { + "@farcaster/core": "^0.13.7", + "@noble/ed25519": "^2.0.0", "happy-dom": "^13.3.8", - "hono-og": "~0.0.2" + "hono-og": "~0.0.2", + "viem": "^2.7.6" } }