Skip to content

Commit

Permalink
feat: cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
jxom committed Feb 9, 2024
1 parent bf514fc commit 2fdc8ab
Show file tree
Hide file tree
Showing 18 changed files with 763 additions and 751 deletions.
6 changes: 3 additions & 3 deletions scripts/preconstruct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ for (const packagePath of packagePaths) {
// Skip `package.json` exports
if (/package\.json$/.test(key)) continue

let entries: string[][]
let entries: any
if (typeof exports === 'string')
entries = [
['default', exports],
Expand All @@ -74,8 +74,8 @@ for (const packagePath of packagePaths) {
path.dirname(value).replace(distDirName, ''),
)
let srcFileName: string
if (key === '.') srcFileName = 'index.tsx'
else srcFileName = path.basename(`${key}.tsx`)
if (key === '.') srcFileName = 'index.ts'
else srcFileName = path.basename(`${key}.ts`)
const srcFilePath = path.resolve(srcDir, srcFileName)

const distDir = path.resolve(dir, path.dirname(value))
Expand Down
81 changes: 81 additions & 0 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { HtmlEscapedString } from 'hono/utils/html'

export type ButtonProps = {
children: string
index?: number | undefined
}

export type ButtonRootProps = ButtonProps & {
action?: 'post' | 'post_redirect'
value?: string | undefined
}

ButtonRoot.__type = 'button'
export function ButtonRoot({
action = 'post',
children,
index = 0,
value,
}: ButtonRootProps) {
return [
<meta
property={`fc:frame:button:${index}`}
content={children}
data-value={value}
/>,
<meta property={`fc:frame:button:${index}:action`} content={action} />,
] as unknown as HtmlEscapedString
}

export type ButtonLinkProps = ButtonProps & {
href: string
}

ButtonLink.__type = 'button'
export function ButtonLink({ children, index = 0, href }: ButtonLinkProps) {
return [
<meta
property={`fc:frame:button:${index}`}
content={children}
data-href={href}
/>,
<meta property={`fc:frame:button:${index}:action`} content="link" />,
<meta property={`fc:frame:button:${index}:target`} content={href} />,
] as unknown as HtmlEscapedString
}

export type ButtonMintProps = ButtonProps & {
target: string
}

ButtonMint.__type = 'button'
export function ButtonMint({ children, index = 0, target }: ButtonMintProps) {
return [
<meta
property={`fc:frame:button:${index}`}
content={children}
data-target={target}
/>,
<meta property={`fc:frame:button:${index}:action`} content="mint" />,
<meta property={`fc:frame:button:${index}:target`} content={target} />,
] as unknown as HtmlEscapedString
}

export type ButtonResetProps = ButtonProps

ButtonReset.__type = 'button'
export function ButtonReset({ children, index = 0 }: ButtonResetProps) {
return (
<meta
property={`fc:frame:button:${index}`}
content={children}
data-type="reset"
/>
)
}

export const Button = Object.assign(ButtonRoot, {
Link: ButtonLink,
Mint: ButtonMint,
Reset: ButtonReset,
})
8 changes: 8 additions & 0 deletions src/components/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type TextInputProps = {
placeholder?: string | undefined
}

TextInput.__type = 'text-input'
export function TextInput({ placeholder }: TextInputProps) {
return <meta property="fc:frame:input:text" content={placeholder} />
}
256 changes: 256 additions & 0 deletions src/farc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import {
FrameActionBody,
Message,
NobleEd25519Signer,
makeFrameAction,
} 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 { jsxRenderer } from 'hono/jsx-renderer'
import { type Env, type Schema } from 'hono/types'

import { Preview, previewStyles } from './preview/components.js'
import { htmlToFrame, htmlToState } from './preview/utils.js'
import {
type FrameContext,
type FrameIntents,
type PreviousFrameContext,
} from './types.js'
import { deserializeJson } from './utils/deserializeJson.js'
import { getFrameContext } from './utils/getFrameContext.js'
import { parseIntents } from './utils/parseIntents.js'
import { serializeJson } from './utils/serializeJson.js'
import { toBaseUrl } from './utils/toBaseUrl.js'

export type FrameHandlerReturnType = {
// TODO: Support `fc:frame:image:aspect_ratio`
image: JSX.Element
intents?: FrameIntents | undefined
}

export class Farc<
E extends Env = Env,
S extends Schema = {},
BasePath extends string = '/',
> extends Hono<E, S, BasePath> {
frame<P extends string>(
path: P,
handler: (
context: FrameContext<P>,
previousContext?: PreviousFrameContext | undefined,
) => FrameHandlerReturnType | Promise<FrameHandlerReturnType>,
) {
// Frame Route (implements GET & POST).
this.use(path, async (c) => {
const query = c.req.query()
const previousContext = query.previousContext
? deserializeJson<PreviousFrameContext>(query.previousContext)
: undefined
const context = await getFrameContext(c, previousContext)

const { intents } = await handler(context, previousContext)
const parsedIntents = intents ? parseIntents(intents) : null

const serializedContext = serializeJson(context)
const serializedPreviousContext = serializeJson({
...context,
intents: parsedIntents,
})

const ogSearch = new URLSearchParams()
if (query.previousContext)
ogSearch.set('previousContext', query.previousContext)
if (serializedContext) ogSearch.set('context', serializedContext)

const postSearch = new URLSearchParams()
if (serializedPreviousContext)
postSearch.set('previousContext', serializedPreviousContext)

return c.render(
<html lang="en">
<head>
<meta property="fc:frame" content="vNext" />
<meta
property="fc:frame:image"
content={`${toBaseUrl(context.url)}/image?${ogSearch.toString()}`}
/>
<meta
property="og:image"
content={`${toBaseUrl(context.url)}/image?${ogSearch.toString()}`}
/>
<meta
property="fc:frame:post_url"
content={`${toBaseUrl(context.url)}?${postSearch}`}
/>
{parsedIntents}

<meta property="farc:context" content={serializedContext} />
{query.previousContext && (
<meta
property="farc:prev_context"
content={query.previousContext}
/>
)}
</head>
</html>,
)
})

// OG Image Route
this.get(`${toBaseUrl(path)}/image`, async (c) => {
const query = c.req.query()
const parsedContext = deserializeJson<FrameContext>(query.context)
const parsedPreviousContext = query.previousContext
? deserializeJson<PreviousFrameContext>(query.previousContext)
: undefined
const { image } = await handler(
{ ...parsedContext, request: c.req },
parsedPreviousContext,
)
return new ImageResponse(image)
})

// Frame Preview Routes
this.use(
`${toBaseUrl(path)}/preview`,
jsxRenderer(
(props) => {
const { children } = props
return (
<html lang="en">
<head>
<title>𝑭𝒂𝒓𝒄 ▶︎ {path}</title>
<style>{previewStyles()}</style>
</head>
<body style={{ padding: '1rem' }}>{children}</body>
</html>
)
},
{ docType: true },
),
)
.get(async (c) => {
const baseUrl = c.req.url.replace('/preview', '')
const response = await fetch(baseUrl)
const text = await response.text()
const frame = htmlToFrame(text)
const state = htmlToState(text)
return c.render(<Preview {...{ baseUrl, frame, state }} />)
})
.post(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)
: '',
)
// TODO: Sanitize input
const inputText = formData.get('inputText')
? Buffer.from(formData.get('inputText') as 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 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(formData.get('action') as string, {
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'),
},
}),
})
const text = await response.text()
// TODO: handle redirects
const frame = htmlToFrame(text)
const state = htmlToState(text)

return c.render(<Preview {...{ baseUrl, frame, state }} />)
})
}
}
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export {
Button,
type ButtonLinkProps,
type ButtonMintProps,
type ButtonProps,
type ButtonResetProps,
} from './components/Button.js'
export { TextInput, type TextInputProps } from './components/TextInput.js'

export { Farc, type FrameHandlerReturnType } from './farc.js'
Loading

0 comments on commit 2fdc8ab

Please sign in to comment.