diff --git a/examples/_dev/src/index.tsx b/examples/_dev/src/index.tsx index dc1ac686..69dc5206 100644 --- a/examples/_dev/src/index.tsx +++ b/examples/_dev/src/index.tsx @@ -101,8 +101,10 @@ app.frame('/buttons', ({ buttonValue }) => { ), intents: [ - , - Google, + , + Link, Mint , diff --git a/src/components/Button.tsx b/src/components/Button.tsx index da90258c..cbb8fd6b 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -7,6 +7,7 @@ export type ButtonProps = { export type ButtonRootProps = ButtonProps & { action?: 'post' | 'post_redirect' + target?: string | undefined value?: string | undefined } @@ -15,6 +16,7 @@ export function ButtonRoot({ action = 'post', children, index = 0, + target, value, }: ButtonRootProps) { return [ @@ -24,6 +26,7 @@ export function ButtonRoot({ data-value={value} />, , + , ] as unknown as HtmlEscapedString } diff --git a/src/dev/components.tsx b/src/dev/components.tsx index ebb04fa2..662ee9ac 100644 --- a/src/dev/components.tsx +++ b/src/dev/components.tsx @@ -72,7 +72,15 @@ function Frame(props: FrameProps) { } = props const hasIntents = Boolean(input || buttons?.length) return ( -
+
@@ -137,11 +145,13 @@ function Input(props: InputProps) { return ( ) } @@ -161,48 +171,94 @@ function Button(props: ButtonProps) { {title} ) + const leavingAppContainerProps = { + class: 'relative', + 'x-data': `{ + index: '${index}', + open: false, + target: ${target ? `'${target}'` : undefined}, + }`, + } + const leavingAppPrompt = ( +
+

Leaving Warpcast

+
{target}
+

+ If you connect your wallet and the site is malicious, you may lose + funds. +

+
+ + +
+
+ ) if (type === 'link') return ( -
+
-
+ ) + + if (type === 'post_redirect') + return ( +
+ - -
-
+
{innerHtml}
+
{redirectIcon}
+ + + {leavingAppPrompt}
) @@ -216,7 +272,6 @@ function Button(props: ButtonProps) { > {type === 'mint' && mintIcon} {innerHtml} - {type === 'post_redirect' && redirectIcon} ) } diff --git a/src/dev/utils.ts b/src/dev/utils.ts index 65aa47d4..02085734 100644 --- a/src/dev/utils.ts +++ b/src/dev/utils.ts @@ -1,5 +1,11 @@ import { Window } from 'happy-dom' import { inspectRoutes } from 'hono/dev' +import { + FrameActionBody, + NobleEd25519Signer, + makeFrameAction, +} from '@farcaster/core' +import { ed25519 } from '@noble/curves/ed25519' import { type FrameContext, @@ -253,3 +259,103 @@ export function getRoutes( } return frameRoutes } + +export function getData(value: Record) { + const buttonIndex = parseInt(value.buttonIndex as string) + if (buttonIndex < 1 || buttonIndex > 4) throw new Error('Invalid buttonIndex') + + const postUrl = value.postUrl as string + if (!postUrl) throw new Error('Invalid postUrl') + + // TODO: Sanitize input + const inputText = value.inputText as string | undefined + + // TODO: Make dynamic + const fid = 2 + const castId = { + fid, + hash: new Uint8Array( + Buffer.from('0000000000000000000000000000000000000000', 'hex'), + ), + } + + return { buttonIndex, castId, fid, inputText, postUrl } +} + +export async function fetchFrameMessage({ + baseUrl, + buttonIndex, + castId, + fid, + inputText, +}: { + baseUrl: string + buttonIndex: number + castId: { + fid: number + hash: Uint8Array + } + fid: number + inputText: string | undefined +}) { + const privateKeyBytes = ed25519.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 frameActionBody = FrameActionBody.create({ + url: Buffer.from(baseUrl), + buttonIndex, + castId, + inputText: inputText ? Buffer.from(inputText) : undefined, + }) + const frameActionMessage = await makeFrameAction( + frameActionBody, + { fid, network: 1 }, + new NobleEd25519Signer(privateKeyBytes), + ) + + return frameActionMessage._unsafeUnwrap() +} diff --git a/src/farc.tsx b/src/farc.tsx index 2ba861eb..524e1984 100644 --- a/src/farc.tsx +++ b/src/farc.tsx @@ -1,20 +1,21 @@ -import { - FrameActionBody, - Message, - NobleEd25519Signer, - makeFrameAction, -} from '@farcaster/core' +import { Message } from '@farcaster/core' import { bytesToHex } from '@noble/curves/abstract/utils' -import { ed25519 } from '@noble/curves/ed25519' import { Hono } from 'hono' import { ImageResponse } from 'hono-og' import { inspectRoutes } from 'hono/dev' import { type HonoOptions } from 'hono/hono-base' import { jsxRenderer } from 'hono/jsx-renderer' import { type Env, type Schema } from 'hono/types' +import { validator } from 'hono/validator' import { Dev, Preview, Style } from './dev/components.js' -import { getRoutes, htmlToFrame, htmlToState } from './dev/utils.js' +import { + fetchFrameMessage, + getData, + getRoutes, + htmlToFrame, + htmlToState, +} from './dev/utils.js' import { type FrameContext, type FrameImageAspectRatio, @@ -234,89 +235,94 @@ export class Farc< return c.render() }) - .post(async (c) => { - const baseUrl = c.req.url.replace('/dev', '') + .post( + validator('form', (value, c) => { + try { + return getData(value) + } catch (e) { + return c.text('Invalid data', 400) + } + }), + async (c) => { + const baseUrl = c.req.url.replace('/dev', '') + const form = c.req.valid('form') + const { buttonIndex, castId, fid, inputText, postUrl } = form + + const message = await fetchFrameMessage({ + baseUrl, + buttonIndex, + castId, + fid, + inputText, + }) + + let response = await fetch(postUrl, { + method: 'POST', + body: JSON.stringify({ + untrustedData: { + buttonIndex, + castId: { + fid: castId.fid, + hash: `0x${bytesToHex(castId.hash)}`, + }, + fid, + inputText: inputText + ? Buffer.from(inputText).toString('utf-8') + : undefined, + messageHash: `0x${bytesToHex(message.hash)}`, + network: 1, + timestamp: message.data.timestamp, + url: baseUrl, + }, + trustedData: { + messageBytes: Buffer.from( + Message.encode(message).finish(), + ).toString('hex'), + }, + }), + }) + + // fetch initial state on error + const error = + response.status !== 200 ? response.statusText : undefined + if (response.status !== 200) response = await fetch(baseUrl) + + const text = await response.text() + // TODO: handle redirects + const frame = htmlToFrame(text) + const state = htmlToState(text) + const routes = getRoutes(baseUrl, inspectRoutes(this.hono)) + + return c.render( + , + ) + }, + ) - const formData = await c.req.formData() - const buttonIndex = parseInt( - typeof formData.get('buttonIndex') === 'string' - ? (formData.get('buttonIndex') as string) - : '', - ) - // TODO: Sanitize input - const inputText = formData.get('inputText') - ? Buffer.from(formData.get('inputText') as string) - : undefined - const postUrl = formData.get('postUrl') as string - - const privateKeyBytes = ed25519.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'), - ), + this.hono.post( + `${parsePath(path)}/dev/redirect`, + validator('json', (value, c) => { + try { + return getData(value) + } catch (e) { + return c.text('Invalid data', 400) } - const frameActionBody = FrameActionBody.create({ - url: Buffer.from(baseUrl), + }), + async (c) => { + const baseUrl = c.req.url.replace('/dev', '') + const json = c.req.valid('json') + const { buttonIndex, castId, fid, inputText, postUrl } = json + + const message = await fetchFrameMessage({ + baseUrl, buttonIndex, castId, + fid, inputText, }) - const frameActionMessage = await makeFrameAction( - frameActionBody, - { fid, network: 1 }, - new NobleEd25519Signer(privateKeyBytes), - ) - const message = frameActionMessage._unsafeUnwrap() - let response = await fetch(postUrl, { + console.log(postUrl) + const response = await fetch(postUrl, { method: 'POST', body: JSON.stringify({ untrustedData: { @@ -342,20 +348,15 @@ export class Farc< }), }) - // fetch initial state on error - const error = response.status !== 200 ? response.statusText : undefined - if (response.status !== 200) response = await fetch(baseUrl) - - const text = await response.text() - // TODO: handle redirects - const frame = htmlToFrame(text) - const state = htmlToState(text) - const routes = getRoutes(baseUrl, inspectRoutes(this.hono)) + // TODO: Get redirect url + console.log({ response }) - return c.render( - , - ) - }) + return c.json({ + success: true, + redirectUrl: '/', + }) + }, + ) } route<