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<