Skip to content

Commit 908bed1

Browse files
authored
feat: mini-app transactions, frog/vercel deprecation (#503)
* 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
1 parent 236323e commit 908bed1

34 files changed

+417
-74
lines changed

.changeset/red-books-tan.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"frog": minor
3+
---
4+
5+
**Breaking Change**: `frog/vercel` was deleted. If you used `handle` from this package, import it from `frog/next`.
6+
**Breaking Change:** `frog/next` no longer exports `postComposerCreateCastActionMessage`. Use `createCast` from `frog/web`.
7+
8+
Introduced `frog/web` for client-side related logic in favor of `frog/next`.
9+
For backwards compatibility, all the previous exports are kept, but will be
10+
deprecated in future, except for NextJS related `handle` function.
11+
12+
Added functionality for the Mini-App JSON-RPC requests. [See more](https://warpcast.notion.site/Miniapp-Transactions-1216a6c0c10180b7b9f4eec58ec51e55).
13+
Added `createCast`, `sendTransaction`, `contractTransaction` and `signTypedData` to `frog/web`.

services/frame/api/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Button, Frog } from 'frog'
22
import { devtools } from 'frog/dev'
3+
import { handle } from 'frog/next'
34
import { serveStatic } from 'frog/serve-static'
4-
import { handle } from 'frog/vercel'
55

66
type State = {
77
featureIndex: number

site/pages/concepts/composer-actions.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ app.composerAction(
5252
```
5353

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

5757
```tsx twoslash [src/index.tsx]
5858
// @noErrors
59-
import { postComposerCreateCastActionMessage } from 'frog/next'
59+
import { createCast } from 'frog/web'
6060

6161
function App() {
6262
return (
63-
<button onClick={() => postComposerCreateCastActionMessage({/**/})}>
63+
<button onClick={() => createCast({/**/})}>
6464
Button
6565
</button>
6666
)

site/pages/platforms/next.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ This leverages the [`generateMetadata`](https://nextjs.org/docs/app/api-referenc
268268

269269
```tsx twoslash [app/page.tsx]
270270
// @noErrors
271-
import { getFrameMetadata } from 'frog/next'
271+
import { getFrameMetadata } from 'frog/web'
272272
import type { Metadata } from 'next'
273273

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

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

site/pages/platforms/vercel.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ After that, we will append Vercel handlers to the file.
9191
/** @jsxImportSource frog/jsx */
9292
// ---cut---
9393
import { Button, Frog } from 'frog'
94-
import { handle } from 'frog/vercel' // [!code focus]
94+
import { handle } from 'frog/next' // [!code focus]
9595
9696
// Uncomment to use Edge Runtime.
9797
// export const config = {
@@ -135,7 +135,7 @@ Add Frog [Devtools](/concepts/devtools) after all frames are defined. This way t
135135
/** @jsxImportSource frog/jsx */
136136
// ---cut---
137137
import { Button, Frog } from 'frog'
138-
import { handle } from 'frog/vercel'
138+
import { handle } from 'frog/next'
139139
import { devtools } from 'frog/dev' // [!code focus]
140140
import { serveStatic } from 'frog/serve-static' // [!code focus]
141141
@@ -210,7 +210,7 @@ they will be redirected to the `/` path.
210210
/** @jsxImportSource frog/jsx */
211211
// ---cut---
212212
import { Button, Frog } from 'frog'
213-
import { handle } from 'frog/vercel'
213+
import { handle } from 'frog/next'
214214

215215
// Uncomment to use Edge Runtime.
216216
// export const config = {

src/vercel/handle.ts renamed to src/next/handle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Schema } from 'hono'
22
import { handle as handle_hono } from 'hono/vercel'
33

4-
import type { Frog } from '../frog.js'
4+
import type { Frog } from '../frog.jsx'
55
import type { Env } from '../types/env.js'
66

77
export function handle<

src/next/index.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1 @@
1-
// TODO: Rename this package to `js` as most of it doesn't strictly depend
2-
// on Next.JS specific features. Only `handle` does.
3-
4-
export { getFrameMetadata } from './getFrameMetadata.js'
5-
export { handle } from '../vercel/index.js'
6-
export { isFrameRequest } from './isFrameRequest.js'
7-
export {
8-
postComposerActionMessage,
9-
postComposerCreateCastActionMessage,
10-
} from './postComposerActionMessage.js'
1+
export { handle } from './handle.js'

src/next/postComposerActionMessage.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/package.json

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@
8484
"types": "./_lib/ui/icons/radix-icons/index.d.ts",
8585
"default": "./_lib/ui/icons/radix-icons/index.js"
8686
},
87-
"./vercel": {
88-
"types": "./_lib/vercel/index.d.ts",
89-
"default": "./_lib/vercel/index.js"
87+
"./web": {
88+
"types": "./_lib/web/index.d.ts",
89+
"default": "./_lib/web/index.js"
9090
}
9191
},
9292
"peerDependencies": {
@@ -121,10 +121,7 @@
121121
"license": "MIT",
122122
"homepage": "https://frog.fm",
123123
"repository": "wevm/frog",
124-
"authors": [
125-
"awkweb.eth",
126-
"jxom.eth"
127-
],
124+
"authors": ["awkweb.eth", "jxom.eth"],
128125
"funding": [
129126
{
130127
"type": "github",

src/vercel/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/vercel/package.json

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
type Abi,
3+
AbiFunctionNotFoundError,
4+
type ContractFunctionArgs,
5+
type ContractFunctionName,
6+
type EncodeFunctionDataParameters,
7+
type GetAbiItemParameters,
8+
encodeFunctionData,
9+
getAbiItem,
10+
} from 'viem'
11+
import type { ContractTransactionParameters } from '../../types/transaction.js'
12+
import type { JsonRpcResponseError } from './internal/jsonRpc/types.js'
13+
import { postSendTransactionRequestMessage } from './internal/postSendTransactionRequestMessage.js'
14+
import {
15+
type EthSendTransactionSuccessBody,
16+
waitForSendTransactionResponse,
17+
} from './internal/waitForSendTransactionResponse.js'
18+
19+
type ContractTransactionReturnType = EthSendTransactionSuccessBody
20+
type ContractTransactionErrorType = JsonRpcResponseError
21+
export type {
22+
ContractTransactionParameters,
23+
ContractTransactionReturnType,
24+
ContractTransactionErrorType,
25+
}
26+
27+
export async function contractTransaction<
28+
const abi extends Abi | readonly unknown[],
29+
functionName extends ContractFunctionName<abi, 'nonpayable' | 'payable'>,
30+
args extends ContractFunctionArgs<
31+
abi,
32+
'nonpayable' | 'payable',
33+
functionName
34+
>,
35+
>(
36+
parameters: ContractTransactionParameters<abi, functionName, args>,
37+
requestIdOverride?: string,
38+
): Promise<ContractTransactionReturnType> {
39+
const { abi, chainId, functionName, gas, to, args, attribution, value } =
40+
parameters
41+
42+
const abiItem = getAbiItem({
43+
abi: abi,
44+
name: functionName,
45+
args,
46+
} as GetAbiItemParameters)
47+
if (!abiItem) throw new AbiFunctionNotFoundError(functionName)
48+
49+
const abiErrorItems = (abi as Abi).filter((item) => item.type === 'error')
50+
51+
const requestId = postSendTransactionRequestMessage(
52+
{
53+
abi: [abiItem, ...abiErrorItems],
54+
attribution,
55+
chainId,
56+
data: encodeFunctionData({
57+
abi,
58+
args,
59+
functionName,
60+
} as EncodeFunctionDataParameters),
61+
gas,
62+
to,
63+
value,
64+
},
65+
requestIdOverride,
66+
)
67+
return waitForSendTransactionResponse(requestId)
68+
}

src/web/actions/createCast.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { JsonRpcResponseError } from './internal/jsonRpc/types.js'
2+
import {
3+
type CreateCastRequestMessageParameters,
4+
postCreateCastRequestMessage,
5+
} from './internal/postCreateCastRequestMessage.js'
6+
import type { FcCreateCastSuccessBody } from './internal/waitForCreateCastResponse.js'
7+
import { waitForCreateCastResponse } from './internal/waitForCreateCastResponse.js'
8+
9+
type CreateCastParameters = CreateCastRequestMessageParameters
10+
type CreateCastReturnType = FcCreateCastSuccessBody
11+
type CreateCastErrorType = JsonRpcResponseError
12+
export type { CreateCastParameters, CreateCastReturnType, CreateCastErrorType }
13+
14+
export async function createCast(
15+
parameters: CreateCastParameters,
16+
requestIdOverride?: string,
17+
): Promise<CreateCastReturnType> {
18+
const requestId = postCreateCastRequestMessage(parameters, requestIdOverride)
19+
return waitForCreateCastResponse(requestId)
20+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export class JsonRpcError extends Error {
2+
code: number
3+
requestId: string
4+
constructor(requestId: string, code: number, message: string) {
5+
super(message)
6+
this.name = 'JsonRpcError'
7+
this.code = code
8+
this.requestId = requestId
9+
}
10+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { JsonRpcResponseFailure, JsonRpcResponseSuccess } from './types.js'
2+
3+
export function listenForJsonRpcResponseMessage<resultType>(
4+
handler: (
5+
message: JsonRpcResponseSuccess<resultType> | JsonRpcResponseFailure,
6+
) => unknown,
7+
requestId: string,
8+
) {
9+
if (typeof window === 'undefined')
10+
throw new Error(
11+
'`listenForJsonRpcResponseMessage` must be called in the Client Component.',
12+
)
13+
14+
const listener = (
15+
event: MessageEvent<
16+
JsonRpcResponseSuccess<resultType> | JsonRpcResponseFailure
17+
>,
18+
) => {
19+
if (event.data.id !== requestId) return
20+
21+
handler(event.data)
22+
}
23+
24+
window.parent.addEventListener('message', listener)
25+
26+
return () => window.parent.removeEventListener('message', listener)
27+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { JsonRpcMethod } from './types.js'
2+
3+
export type PostJsonRpcRequestMessageReturnType = string
4+
5+
export function postJsonRpcRequestMessage(
6+
method: JsonRpcMethod,
7+
parameters: any,
8+
requestIdOverride?: string,
9+
): PostJsonRpcRequestMessageReturnType {
10+
if (typeof window === 'undefined')
11+
throw new Error(
12+
'`postJsonRpcRequestMessage` must be called in the Client Component.',
13+
)
14+
15+
const requestId = requestIdOverride ?? crypto.randomUUID()
16+
window.parent.postMessage(
17+
{
18+
jsonrpc: '2.0',
19+
id: requestId,
20+
method,
21+
params: parameters,
22+
},
23+
'*',
24+
)
25+
return requestId
26+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export type JsonRpcResponseSuccess<resultType> = {
2+
jsonrpc: '2.0'
3+
id: string | number | null
4+
result: resultType
5+
}
6+
7+
export type JsonRpcResponseError = {
8+
code: number
9+
message: string
10+
}
11+
12+
export type JsonRpcResponseFailure = {
13+
jsonrpc: '2.0'
14+
id: string | number | null
15+
error: JsonRpcResponseError
16+
}
17+
18+
export type JsonRpcMethod = 'fc_requestWalletAction' | 'fc_createCast'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { JsonRpcError } from './errors.js'
2+
import { listenForJsonRpcResponseMessage } from './listenForJsonRpcResponseMessage.js'
3+
4+
export function waitForJsonRpcResponse<resultType>(
5+
requestId: string,
6+
): Promise<resultType> {
7+
return new Promise<resultType>((resolve, reject) => {
8+
listenForJsonRpcResponseMessage<resultType>((message) => {
9+
if ('result' in message) {
10+
resolve(message.result)
11+
return
12+
}
13+
reject(
14+
new JsonRpcError(requestId, message.error.code, message.error.message),
15+
)
16+
}, requestId)
17+
})
18+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {
2+
type PostJsonRpcRequestMessageReturnType,
3+
postJsonRpcRequestMessage,
4+
} from './jsonRpc/postJsonRpcRequestMessage.js'
5+
6+
export type CreateCastRequestMessageParameters = {
7+
embeds: string[]
8+
text: string
9+
}
10+
11+
export type CreateCastRequestMessageReturnType =
12+
PostJsonRpcRequestMessageReturnType
13+
14+
export function postCreateCastRequestMessage(
15+
parameters: CreateCastRequestMessageParameters,
16+
requestIdOverride?: string,
17+
) {
18+
return postJsonRpcRequestMessage(
19+
'fc_createCast',
20+
parameters,
21+
requestIdOverride,
22+
)
23+
}

0 commit comments

Comments
 (0)