Skip to content

Transactions

Farcaster Frames have the ability to instruct an App to invoke and perform Ethereum transactions (see the spec).

Overview

At a glance:

  1. A Frame has a <Button.Transaction> element with a specified target .transaction route.
  2. When the user presses the button in the App, the App will make a POST request to the .transaction route.
  3. The App uses the response to forward the transaction data to the user's wallet for signing and broadcasting.
  4. Once the user has sent the transaction, the App will perform a POST request to the frame.

Walkthrough

Here is a trivial example on how to expose a transaction interface in a frame. We will break it down below.

src/index.tsx
import { Button, Frog, TextInput, parseEther } from 'frog'
import { abi } from './abi'
 
export const app = new Frog({ title: 'Frog Frame' })
 
app.frame('/', (c) => {
  return c.res({
    action: '/finish',
    image: (
      <div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
        Perform a transaction
      </div>
    ),
    intents: [
      <TextInput placeholder="Value (ETH)" />,
      <Button.Transaction target="/send-ether">Send Ether</Button.Transaction>,
      <Button.Transaction target="/mint">Mint</Button.Transaction>,
    ]
  })
})
 
app.frame('/finish', (c) => {
  const { transactionId } = c
  return c.res({
    image: (
      <div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
        Transaction ID: {transactionId}
      </div>
    )
  })
})
 
app.transaction('/send-ether', (c) => {
  const { inputText } = c
  // Send transaction response.
  return c.send({
    chainId: 'eip155:10',
    to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
    value: parseEther(inputText),
  })
})
 
app.transaction('/mint', (c) => {
  const { inputText } = c
  // Contract transaction response.
  return c.contract({
    abi,
    chainId: 'eip155:10',
    functionName: 'mint',
    args: [69420n],
    to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
    value: parseEther(inputText)
  })
})

1. Render Frame & Intents

In the example above, we are rendering three transaction intents:

  1. A text input to capture the amount of ether to send with the transaction.
  2. A "Send Ether" button that will call the /send-ether route, and expose a "send ethereum to an address" interface to the App.
  3. A "Mint" button that will call the /mint route, and expose a "mint NFT" interface to the App.
src/index.tsx
app.frame('/', (c) => {
  return c.res({
    image: (
      <div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
        Perform a transaction
      </div>
    ),
    intents: [
      <TextInput placeholder="Value (ETH)" />,
      <Button.Transaction target="/send-ether">Send Ether</Button.Transaction>,
      <Button.Transaction target="/mint">Mint</Button.Transaction>,
    ]
  })
})
 
// ...

2. Handle /send-ether Requests

Without route handlers to handle these requests, these buttons will be meaningless.

Firstly, let's define a /send-ether route to handle the "Send Ether" button:

src/index.tsx
app.frame('/', (c) => {
  return c.res({
    image: (
      <div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
        Perform a transaction
      </div>
    ),
    intents: [
      <TextInput placeholder="Value (ETH)" />,
      <Button.Transaction target="/send-ether">Send Ether</Button.Transaction>,
      <Button.Transaction target="/mint">Mint</Button.Transaction>,
    ]
  })
})
 
// ...
 
app.transaction('/send-ether', (c) => {
  const { inputText } = c
  // Send transaction response.
  return c.send({
    chainId: 'eip155:10',
    to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
    value: parseEther(inputText),
  })
})

A breakdown of the /send-ether route handler:

  • We are responding with a c.send ("send transaction") response.
  • We are extracting user input from the previous frame via inputText.
  • Within c.send, we can specify a:
    • chainId: CAIP-2 compliant chain ID. We are sending to eip155:10 where 1 is Ethereum mainnet.
    • to: a recipient.
    • value: the amount of wei to send. We are using parseEther to convert ether → wei.
    • data: optional calldata to include in the transaction.
    • abi: optional ABI to include in the transaction.
  • The c.send function constructs a well-formed JSON response to be consumed by the App.

3. Handle /mint Requests

Secondly, let's define a /mint route to handle the "Mint" button:

src/index.tsx
import { abi } from './abi'
 
app.frame('/', (c) => {
  return c.res({
    image: (
      <div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
        Perform a transaction
      </div>
    ),
    intents: [
      <TextInput placeholder="Value (ETH)" />,
      <Button.Transaction target="/send-ether">Send Ether</Button.Transaction>,
      <Button.Transaction target="/mint">Mint</Button.Transaction>,
    ]
  })
})
 
// ...
 
app.transaction('/mint', (c) => {
  const { inputText } = c
  // Contract transaction response.
  return c.contract({
    abi,
    functionName: 'mint',
    args: [69420n],
    chainId: 'eip155:10',
    to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
    value: parseEther(inputText)
  })
})

A breakdown of the /mint route handler:

  • We are responding with a c.contract ("contract transaction") response.
  • We are extracting user input from the previous frame via inputText.
  • Within c.contract, we can specify a:
    • abi: ABI for the contract.
    • functionName: Function to call on the contract.
    • args: Arguments to supply to the function.
    • chainId: CAIP-2 compliant chain ID.
    • to: Contract address.
    • value: Optional amount of wei to send to the payable function.
  • The c.contract function constructs a well-formed JSON response to be consumed by the App.

4. Handle Post-Transaction Execution

Once the user has sent the transaction, the App will perform a POST request to the frame.

We can extract the transaction ID from context via c.transactionId.

src/index.tsx
app.frame('/', (c) => {
  return c.res({
    action: '/finish',
    image: (
      <div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
        Perform a transaction
      </div>
    ),
    intents: [
      <Button.Transaction target="/send-ether">Send Ether</Button.Transaction>,
      <Button.Transaction target="/mint">Mint</Button.Transaction>,
    ]
  })
})
 
app.frame('/finish', (c) => {
  const { transactionId } = c
  return c.res({
    image: (
      <div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
        Transaction ID: {transactionId}
      </div>
    )
  })
})
 
app.transaction('/send-ether', (c) => {
  // Send transaction response.
  return c.send({
    chainId: 'eip155:10',
    to: '0xd2135CfB216b74109775236E36d4b433F1DF507B',
    value: parseEther('1'),
  })
})
 
app.transaction('/mint', (c) => {
  // Contract transaction response.
  return c.contract({
    abi,
    chainId: 'eip155:10',
    functionName: 'mint',
    args: [69420n],
    to: '0xd2135CfB216b74109775236E36d4b433F1DF507B'
  })
})

5. Bonus: Learn the API