Skip to content

Commit

Permalink
feat: mini-app transactions, frog/vercel deprecation (wevm#503)
Browse files Browse the repository at this point in the history
* feat: mini-app transaction requests and response listening

* nit: lint

* refactor: jsonrpc, split next and web

* nit: lint

* feat: `JsonRpcError` added

* nit: lint

* nit: better error

* nit: error

* chore: changesets

* chore: up changeset

* chore: changesets up

* nit: remove `frog/next`

* nit: stuff

* nit: lint
  • Loading branch information
dalechyn authored Oct 24, 2024
1 parent 236323e commit 908bed1
Show file tree
Hide file tree
Showing 34 changed files with 417 additions and 74 deletions.
13 changes: 13 additions & 0 deletions .changeset/red-books-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"frog": minor
---

**Breaking Change**: `frog/vercel` was deleted. If you used `handle` from this package, import it from `frog/next`.
**Breaking Change:** `frog/next` no longer exports `postComposerCreateCastActionMessage`. Use `createCast` from `frog/web`.

Introduced `frog/web` for client-side related logic in favor of `frog/next`.
For backwards compatibility, all the previous exports are kept, but will be
deprecated in future, except for NextJS related `handle` function.

Added functionality for the Mini-App JSON-RPC requests. [See more](https://warpcast.notion.site/Miniapp-Transactions-1216a6c0c10180b7b9f4eec58ec51e55).
Added `createCast`, `sendTransaction`, `contractTransaction` and `signTypedData` to `frog/web`.
2 changes: 1 addition & 1 deletion services/frame/api/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button, Frog } from 'frog'
import { devtools } from 'frog/dev'
import { handle } from 'frog/next'
import { serveStatic } from 'frog/serve-static'
import { handle } from 'frog/vercel'

type State = {
featureIndex: number
Expand Down
6 changes: 3 additions & 3 deletions site/pages/concepts/composer-actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ app.composerAction(
```

### Client-Side Helpers
Frog exports `postComposerCreateCastActionMessage` helper to post the message to the `window.parent`.
Frog exports `createCast` helper to post the message to the `window.parent`.

```tsx twoslash [src/index.tsx]
// @noErrors
import { postComposerCreateCastActionMessage } from 'frog/next'
import { createCast } from 'frog/web'

function App() {
return (
<button onClick={() => postComposerCreateCastActionMessage({/**/})}>
<button onClick={() => createCast({/**/})}>
Button
</button>
)
Expand Down
4 changes: 2 additions & 2 deletions site/pages/platforms/next.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ This leverages the [`generateMetadata`](https://nextjs.org/docs/app/api-referenc

```tsx twoslash [app/page.tsx]
// @noErrors
import { getFrameMetadata } from 'frog/next'
import { getFrameMetadata } from 'frog/web'
import type { Metadata } from 'next'

export async function generateMetadata(): Promise<Metadata> {
Expand All @@ -291,7 +291,7 @@ If you use suspended components in a page, route Next.js will stream the respons
// @noErrors
import { headers } from 'next/headers'
import type { Metadata } from 'next'
import { getFrameMetadata, isFrameRequest } from 'frog/next'
import { getFrameMetadata, isFrameRequest } from 'frog/web'

import { SuspendedComponent } from './suspense-component'

Expand Down
6 changes: 3 additions & 3 deletions site/pages/platforms/vercel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ After that, we will append Vercel handlers to the file.
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'
import { handle } from 'frog/vercel' // [!code focus]
import { handle } from 'frog/next' // [!code focus]
// Uncomment to use Edge Runtime.
// export const config = {
Expand Down Expand Up @@ -135,7 +135,7 @@ Add Frog [Devtools](/concepts/devtools) after all frames are defined. This way t
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'
import { handle } from 'frog/vercel'
import { handle } from 'frog/next'
import { devtools } from 'frog/dev' // [!code focus]
import { serveStatic } from 'frog/serve-static' // [!code focus]
Expand Down Expand Up @@ -210,7 +210,7 @@ they will be redirected to the `/` path.
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'
import { handle } from 'frog/vercel'
import { handle } from 'frog/next'

// Uncomment to use Edge Runtime.
// export const config = {
Expand Down
2 changes: 1 addition & 1 deletion src/vercel/handle.ts → src/next/handle.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Schema } from 'hono'
import { handle as handle_hono } from 'hono/vercel'

import type { Frog } from '../frog.js'
import type { Frog } from '../frog.jsx'
import type { Env } from '../types/env.js'

export function handle<
Expand Down
11 changes: 1 addition & 10 deletions src/next/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1 @@
// TODO: Rename this package to `js` as most of it doesn't strictly depend
// on Next.JS specific features. Only `handle` does.

export { getFrameMetadata } from './getFrameMetadata.js'
export { handle } from '../vercel/index.js'
export { isFrameRequest } from './isFrameRequest.js'
export {
postComposerActionMessage,
postComposerCreateCastActionMessage,
} from './postComposerActionMessage.js'
export { handle } from './handle.js'
37 changes: 0 additions & 37 deletions src/next/postComposerActionMessage.ts

This file was deleted.

11 changes: 4 additions & 7 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@
"types": "./_lib/ui/icons/radix-icons/index.d.ts",
"default": "./_lib/ui/icons/radix-icons/index.js"
},
"./vercel": {
"types": "./_lib/vercel/index.d.ts",
"default": "./_lib/vercel/index.js"
"./web": {
"types": "./_lib/web/index.d.ts",
"default": "./_lib/web/index.js"
}
},
"peerDependencies": {
Expand Down Expand Up @@ -121,10 +121,7 @@
"license": "MIT",
"homepage": "https://frog.fm",
"repository": "wevm/frog",
"authors": [
"awkweb.eth",
"jxom.eth"
],
"authors": ["awkweb.eth", "jxom.eth"],
"funding": [
{
"type": "github",
Expand Down
1 change: 0 additions & 1 deletion src/vercel/index.ts

This file was deleted.

5 changes: 0 additions & 5 deletions src/vercel/package.json

This file was deleted.

68 changes: 68 additions & 0 deletions src/web/actions/contractTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
type Abi,
AbiFunctionNotFoundError,
type ContractFunctionArgs,
type ContractFunctionName,
type EncodeFunctionDataParameters,
type GetAbiItemParameters,
encodeFunctionData,
getAbiItem,
} from 'viem'
import type { ContractTransactionParameters } from '../../types/transaction.js'
import type { JsonRpcResponseError } from './internal/jsonRpc/types.js'
import { postSendTransactionRequestMessage } from './internal/postSendTransactionRequestMessage.js'
import {
type EthSendTransactionSuccessBody,
waitForSendTransactionResponse,
} from './internal/waitForSendTransactionResponse.js'

type ContractTransactionReturnType = EthSendTransactionSuccessBody
type ContractTransactionErrorType = JsonRpcResponseError
export type {
ContractTransactionParameters,
ContractTransactionReturnType,
ContractTransactionErrorType,
}

export async function contractTransaction<
const abi extends Abi | readonly unknown[],
functionName extends ContractFunctionName<abi, 'nonpayable' | 'payable'>,
args extends ContractFunctionArgs<
abi,
'nonpayable' | 'payable',
functionName
>,
>(
parameters: ContractTransactionParameters<abi, functionName, args>,
requestIdOverride?: string,
): Promise<ContractTransactionReturnType> {
const { abi, chainId, functionName, gas, to, args, attribution, value } =
parameters

const abiItem = getAbiItem({
abi: abi,
name: functionName,
args,
} as GetAbiItemParameters)
if (!abiItem) throw new AbiFunctionNotFoundError(functionName)

const abiErrorItems = (abi as Abi).filter((item) => item.type === 'error')

const requestId = postSendTransactionRequestMessage(
{
abi: [abiItem, ...abiErrorItems],
attribution,
chainId,
data: encodeFunctionData({
abi,
args,
functionName,
} as EncodeFunctionDataParameters),
gas,
to,
value,
},
requestIdOverride,
)
return waitForSendTransactionResponse(requestId)
}
20 changes: 20 additions & 0 deletions src/web/actions/createCast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { JsonRpcResponseError } from './internal/jsonRpc/types.js'
import {
type CreateCastRequestMessageParameters,
postCreateCastRequestMessage,
} from './internal/postCreateCastRequestMessage.js'
import type { FcCreateCastSuccessBody } from './internal/waitForCreateCastResponse.js'
import { waitForCreateCastResponse } from './internal/waitForCreateCastResponse.js'

type CreateCastParameters = CreateCastRequestMessageParameters
type CreateCastReturnType = FcCreateCastSuccessBody
type CreateCastErrorType = JsonRpcResponseError
export type { CreateCastParameters, CreateCastReturnType, CreateCastErrorType }

export async function createCast(
parameters: CreateCastParameters,
requestIdOverride?: string,
): Promise<CreateCastReturnType> {
const requestId = postCreateCastRequestMessage(parameters, requestIdOverride)
return waitForCreateCastResponse(requestId)
}
10 changes: 10 additions & 0 deletions src/web/actions/internal/jsonRpc/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export class JsonRpcError extends Error {
code: number
requestId: string
constructor(requestId: string, code: number, message: string) {
super(message)
this.name = 'JsonRpcError'
this.code = code
this.requestId = requestId
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { JsonRpcResponseFailure, JsonRpcResponseSuccess } from './types.js'

export function listenForJsonRpcResponseMessage<resultType>(
handler: (
message: JsonRpcResponseSuccess<resultType> | JsonRpcResponseFailure,
) => unknown,
requestId: string,
) {
if (typeof window === 'undefined')
throw new Error(
'`listenForJsonRpcResponseMessage` must be called in the Client Component.',
)

const listener = (
event: MessageEvent<
JsonRpcResponseSuccess<resultType> | JsonRpcResponseFailure
>,
) => {
if (event.data.id !== requestId) return

handler(event.data)
}

window.parent.addEventListener('message', listener)

return () => window.parent.removeEventListener('message', listener)
}
26 changes: 26 additions & 0 deletions src/web/actions/internal/jsonRpc/postJsonRpcRequestMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { JsonRpcMethod } from './types.js'

export type PostJsonRpcRequestMessageReturnType = string

export function postJsonRpcRequestMessage(
method: JsonRpcMethod,
parameters: any,
requestIdOverride?: string,
): PostJsonRpcRequestMessageReturnType {
if (typeof window === 'undefined')
throw new Error(
'`postJsonRpcRequestMessage` must be called in the Client Component.',
)

const requestId = requestIdOverride ?? crypto.randomUUID()
window.parent.postMessage(
{
jsonrpc: '2.0',
id: requestId,
method,
params: parameters,
},
'*',
)
return requestId
}
18 changes: 18 additions & 0 deletions src/web/actions/internal/jsonRpc/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type JsonRpcResponseSuccess<resultType> = {
jsonrpc: '2.0'
id: string | number | null
result: resultType
}

export type JsonRpcResponseError = {
code: number
message: string
}

export type JsonRpcResponseFailure = {
jsonrpc: '2.0'
id: string | number | null
error: JsonRpcResponseError
}

export type JsonRpcMethod = 'fc_requestWalletAction' | 'fc_createCast'
18 changes: 18 additions & 0 deletions src/web/actions/internal/jsonRpc/waitForJsonRpcResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { JsonRpcError } from './errors.js'
import { listenForJsonRpcResponseMessage } from './listenForJsonRpcResponseMessage.js'

export function waitForJsonRpcResponse<resultType>(
requestId: string,
): Promise<resultType> {
return new Promise<resultType>((resolve, reject) => {
listenForJsonRpcResponseMessage<resultType>((message) => {
if ('result' in message) {
resolve(message.result)
return
}
reject(
new JsonRpcError(requestId, message.error.code, message.error.message),
)
}, requestId)
})
}
23 changes: 23 additions & 0 deletions src/web/actions/internal/postCreateCastRequestMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
type PostJsonRpcRequestMessageReturnType,
postJsonRpcRequestMessage,
} from './jsonRpc/postJsonRpcRequestMessage.js'

export type CreateCastRequestMessageParameters = {
embeds: string[]
text: string
}

export type CreateCastRequestMessageReturnType =
PostJsonRpcRequestMessageReturnType

export function postCreateCastRequestMessage(
parameters: CreateCastRequestMessageParameters,
requestIdOverride?: string,
) {
return postJsonRpcRequestMessage(
'fc_createCast',
parameters,
requestIdOverride,
)
}
Loading

0 comments on commit 908bed1

Please sign in to comment.