Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 1 addition & 51 deletions examples/vanilla/frame/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,56 +11,6 @@ <h1>Vanilla Frame</h1>
</header>
<button id="sign">sign hello</button>
<button id="compose-cast">compose cast</button>
<script type="module">
import { sdk } from 'https://esm.sh/@farcaster/frame-sdk'
import { createStore } from 'mipd'

const store = createStore()

let providers = store.getProviders()
store.subscribe((providerDetails) => {
providers = providerDetails
console.debug('updated providers', providers)
})

setTimeout(() => {
sdk.actions.ready()
Promise.race([
sdk.context,
new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('timed out waiting context'))
}, 50)
}),
])
.then((ctx) => {
console.log(ctx)
})
.catch((e) => {
console.warn(e.message)
})

document.querySelector('#sign').onclick = () => {
sdk.wallet.ethProvider
.request({ method: 'eth_requestAccounts' })
.then((addresses) => {
return sdk.wallet.ethProvider.request({
method: 'personal_sign',
params: [
'0x48656c6c6f2066726f6d2056616e696c6c61204672616d65',
addresses[0],
],
})
})
.then((signature) => {
alert('You signed:\n' + signature)
})
}

document.querySelector('#compose-cast').onclick = async () => {
await sdk.actions.composeCast({ close: true })
}
}, 750)
</script>
<script type="module" src=".//index.ts"></script>
</body>
</html>
17 changes: 9 additions & 8 deletions examples/vanilla/frame/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ store.subscribe((providerDetails) => {

setTimeout(() => {
sdk.actions.ready()
Promise.race([
sdk.context,
new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('timed out waiting context'))
}, 50)
}),
])

sdk.setShareStateProvider(() => {
return {
path: 'https://www.youtube.com/watch',
params: 'v=dQw4w9WgXcQ',
}
})

Promise.race([sdk.context])
.then((ctx) => {
// biome-ignore lint/suspicious/noConsoleLog: <explanation>
console.log(ctx)
Expand Down
1 change: 1 addition & 0 deletions examples/vanilla/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<title>Vanilla Frame Host</title>
</head>
<body>
<button id="share">share</button>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
Expand Down
14 changes: 13 additions & 1 deletion examples/vanilla/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type FrameHost, exposeToIframe } from '@farcaster/frame-host'
import { type FrameHost, Rpc, exposeToIframe } from '@farcaster/frame-host'
import './style.css'

declare global {
Expand Down Expand Up @@ -52,4 +52,16 @@ const { endpoint } = exposeToIframe({
sdk: frameHost,
ethProvider: window.ethereum,
frameOrigin: window.origin,
debug: true,
})

const appProviderClient = Rpc.createClient<Rpc.AppProviderSchema>({
endpoint: endpoint as Rpc.Endpoint,
channelName: 'appProvider',
})

document.querySelector<HTMLButtonElement>('#share')!.onclick = async () => {
const result = await appProviderClient.request({
method: 'get_share_state',
})
}
1 change: 1 addition & 0 deletions packages/frame-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * as Manifest from './manifest'
export * from './types'
export * from './schemas'
export * from './funcs'
export * as Rpc from './rpc'
178 changes: 178 additions & 0 deletions packages/frame-core/src/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {
Provider,
RpcRequest,
RpcResponse,
type RpcSchema,
type RpcTransport,
} from 'ox'
import type { ShareState } from './funcs'

export interface EventSource {
addEventListener(
type: string,
listener: (event: MessageEvent) => void,
options?: {},
): void

removeEventListener(
type: string,
listener: (event: MessageEvent) => void,
options?: {},
): void
}

export interface Endpoint extends EventSource {
postMessage(data?: any): void
}

export function createClient<schema extends RpcSchema.Generic>({
channelName,
endpoint,
origin,
}: {
channelName: string
endpoint: Endpoint
origin?: string
}) {
const pendingRequestCallbacks: Record<string, (response: unknown) => void> =
{}
const store = RpcRequest.createStore<schema>()

const request: RpcTransport.RequestFn<false, {}, schema> = async (
parameters,
) => {
return new Promise((resolve, reject) => {
const request = store.prepare(parameters)

pendingRequestCallbacks[request.id] = (response) => {
try {
resolve(
RpcResponse.parse(response, {
request,
}) as never,
)
} catch (error) {
reject(error)
}
}

endpoint.postMessage({
[channelName]: request,
})
})
}

function handleMessage(
event: MessageEvent<Record<string, RpcResponse.RpcResponse>>,
) {
if (event.origin !== origin) {
return
}

const message = event.data
if (message[channelName]) {
const response = message[channelName]
const callback = pendingRequestCallbacks[response.id]
if (callback) {
delete pendingRequestCallbacks[response.id]
return callback(response)
}
}
}

function destroy() {
endpoint.removeEventListener('message', handleMessage)

for (const [id, cb] of Object.entries(pendingRequestCallbacks)) {
cb({
id: Number(id),
jsonrpc: '2.0',
error: {
code: RpcResponse.InternalError.code,
message: 'Client destroyed',
},
})
}
}

endpoint.addEventListener('message', handleMessage)

return {
request,
destroy,
}
}

export function createServer<schema extends RpcSchema.Generic>({
channelName,
endpoint,
handleRequest,
}: {
channelName: string
endpoint: Endpoint
handleRequest: RpcTransport.RequestFn<false, {}, schema>
}) {
function handleMessage(
event: MessageEvent<Record<string, RpcRequest.RpcRequest>>,
) {
const message = event.data
if (message[channelName]) {
const request = message[channelName]
;(async () => {
const response = await (async () => {
try {
const result = await handleRequest(request as never)
return RpcResponse.from({ result }, { request })
} catch (e) {
if (
e instanceof RpcResponse.BaseError ||
e instanceof Provider.ProviderRpcError
) {
return {
id: request.id,
jsonrpc: request.jsonrpc,
error: {
code: e.code,
message: e.message,
},
}
}

return {
id: request.id,
jsonrpc: request.jsonrpc,
error: {
code: RpcResponse.InternalError.code,
message: (e as Error).message,
},
}
}
})()

endpoint.postMessage({
[channelName]: response,
})
})()
}

return
}

endpoint.addEventListener('message', handleMessage)

function close() {
endpoint.removeEventListener('message', handleMessage)
}

return {
close,
}
}

export type AppProviderSchema = RpcSchema.From<{
Request: {
method: 'get_share_state'
params?: undefined
}
ReturnType: ShareState
}>
2 changes: 1 addition & 1 deletion packages/frame-host/src/helpers/endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FrameHost } from '@farcaster/frame-core'
import { type FrameHost, Rpc } from '@farcaster/frame-core'
import type * as Provider from 'ox/Provider'
import { useEffect } from 'react'
import * as Comlink from '../comlink'
Expand Down
42 changes: 40 additions & 2 deletions packages/frame-sdk/src/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
AddFrame,
type FrameClientEvent,
Rpc,
type ShareStateProvider,
SignIn,
} from '@farcaster/frame-core'
import { proxy } from 'comlink'
import { EventEmitter } from 'eventemitter3'
import { RpcResponse, type RpcTransport } from 'ox'
import { endpoint } from './endpoint'
import { frameHost } from './frameHost'
import { provider } from './provider'
import type { Emitter, EventMap, FrameSDK } from './types'
Expand Down Expand Up @@ -77,6 +79,36 @@ async function isInMiniApp(timeoutMs = 50): Promise<boolean> {
return isInMiniApp
}

type RequestFn = RpcTransport.RequestFn<false, {}, Rpc.AppProviderSchema>

const createAppProvider = () => {
let requestFn: RequestFn = () => {
throw new Error('How to handle init state?')
}

Rpc.createServer<Rpc.AppProviderSchema>({
endpoint: endpoint as Rpc.Endpoint,
channelName: 'appProvider',
handleRequest(request) {
if (!requestFn) {
throw new Error('No requestHandler set')
}

return requestFn(request as never)
},
})

function setRequestHandler(fn: RequestFn) {
requestFn = fn
}

return {
setRequestHandler,
}
}

const appProvider = createAppProvider()

export const sdk: FrameSDK = {
...emitter,
isInMiniApp,
Expand Down Expand Up @@ -131,7 +163,13 @@ export const sdk: FrameSDK = {
ethProvider: provider,
},
setShareStateProvider: (fn: ShareStateProvider) => {
frameHost.setShareStateProvider.bind(frameHost)(proxy(fn))
appProvider.setRequestHandler(async (request) => {
if (request.method === 'get_share_state') {
return fn() as never
}

throw new RpcResponse.MethodNotFoundError()
})
},
}

Expand Down
Loading