Skip to content

Permissions

Demo
pnpx gitpick ithacaxyz/porto/tree/main/examples/permissions
Source

Steps

Connect Account

Follow the Onboard & Discover Accounts guide to get this set up.

Setup Permissions

The permissions are setup such that the user can spend 10 EXP per hour. We can either use the useGrantPermissions hook to grant the permissions, or we can add the permissions to the connect function. We will do the latter.

permissions.ts
import { Value } from 'ox'
import { expConfig } from './abi'
 
export const permissions = () =>
  ({
    expiry: Math.floor(Date.now() / 1_000) + 60 * 60, // 1 hour
    permissions: {
      calls: [{ to: expConfig.address }],
      spend: [
        {
          limit: Value.fromEther('10'),
          period: 'hour',
          token: expConfig.address,
        },
      ],
    },
  }) as const

Add permissions to connect

We will reuse the same connect component from the Onboard & Discover Accounts guide with one change: we will add the permissions to the connect function.

Connect.tsx
import { useConnect } from 'wagmi'
import { permissions } from './permissions'
 
export function Connect() {
  const { connectors, connect } = useConnect()
 
  const connector = connectors.find(
    (connector) => connector.id === 'xyz.ithaca.porto',
  )!
 
  return (
    <button
      onClick={() =>
        connect.connect({
          connector,
          capabilities: { 
            grantPermissions: permissions(), 
          }, 
        })
      }
      type="button"
    >
      Sign in
    </button>
  )
}

Create SendTip Component

We will add a simple "Send Tip" button that will trigger the permissions flow. creatorAddress is the address to whom the tip will be sent.

SendTip.tsx
import * as React from 'react'
import { useAccount } from 'wagmi'
 
export function SendTip() {
  const { address } = useAccount()
  const creatorAddress = '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'
 
  return (
    <form>
      <button type="submit">Send Tip</button>
    </form>
  )
}

Hook up useSendCalls

Next, we will add the useSendCalls hook to submit a batch of contract calls.

  • For the first call, we will request for the user to allow us to spend 1 EXP (a payment hold),
  • For the second call, we will transfer the 1 EXP from the user's account to the creator.
SendTip.tsx
import * as React from 'react'
import { useAccount } from 'wagmi'
import { useAccount, useSendCalls } from 'wagmi'
import { parseEther } from 'viem'
import { expConfig } from './abi'
 
export function SendTip() {
  const { address } = useAccount()
  const creatorAddress = '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'
 
  const { sendCalls } = useSendCalls() 
 
  async function submit(event: React.FormEvent<HTMLFormElement>) { 
    event.preventDefault() 
    const shared = { 
      abi: expConfig.abi, 
      to: expConfig.address, 
    } 
    const amount = Value.fromEther('1') 
    sendCalls({ 
      calls: [ 
        { 
          ...shared, 
          args: [address!, amount], 
          functionName: 'approve', 
        }, 
        { 
          ...shared, 
          args: [address!, creatorAddress, amount], 
          functionName: 'transferFrom', 
        }, 
      ], 
    }) 
  } 
 
  return (
    <form> 
    <form onSubmit={submit}> 
      <button type="submit">Buy Now</button>
    </form>
  )
}

Add Pending State

We will also display the pending state to the user while we are waiting for them to approve the request.

SendTip.tsx
import * as React from 'react'
import { useAccount, useSendCalls } from 'wagmi'
import { parseEther } from 'viem'
import { expConfig } from './abi'
 
export function SendTip() {
  const { address } = useAccount()
  const creatorAddress = '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'
 
  const { isPending, sendCalls } = useSendCalls()
 
  async function submit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const shared = {
      abi: expConfig.abi,
      to: expConfig.address,
    }
    const amount = Value.fromEther('1')
    sendCalls({
      calls: [
        {
          ...shared,
          args: [address!, amount],
          functionName: 'approve',
        },
        {
          ...shared,
          args: [address!, creatorAddress, amount],
          functionName: 'transferFrom',
        },
      ],
    })
  }
 
  return (
    <form onSubmit={submit}>
      <button
        disabled={isPending}
        type="submit"
      >
        Tip creator 
        {isPending ? 'Check prompt' : 'Tip creator'}
      </button>
    </form>
  )
}

Hook up useWaitForCallsStatus

Now that we have the calls submitted, we can hook up the useWaitForCallsStatus hook to wait for the calls to be confirmed, and show a "Tipping creator" message to the user.

SendTip.tsx
import * as React from 'react'
import { useAccount, useSendCalls } from 'wagmi'
import { useAccount, useSendCalls, useWaitForCallsStatus } from 'wagmi'
import { parseEther } from 'viem'
import { expConfig } from './abi'
 
export function SendTip() {
  const { address } = useAccount()
  const creatorAddress = '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'
 
  const {
    data, 
    isPending,
    sendCalls
  } = useSendCalls()
 
  const { isLoading: isConfirming } = useWaitForCallsStatus({ 
    id: data?.id,  
  })  
 
  async function submit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const shared = {
      abi: expConfig.abi,
      to: expConfig.address,
    }
    const amount = Value.fromEther('1')
    sendCalls({
      calls: [
        {
          ...shared,
          args: [address!, amount],
          functionName: 'approve',
        },
        {
          ...shared,
          args: [address!, creatorAddress, amount],
          functionName: 'transferFrom',
        },
      ],
    })
  }
 
  return (
    <form onSubmit={submit}>
      <button disabled={isPending || isConfirming} type="submit">
        {isPending ? ( {
          'Check prompt' {
        ) : isConfirming ? ( 
          'Tipping creator…'
        ) : ( 
          'Tip creator'
        )}
      </button>
    </form>
  )
}

Display Success State

SendTip.tsx
import * as React from 'react'
import { parseEther } from 'viem'
import { useAccount, useSendCalls, useWaitForCallsStatus } from 'wagmi'
import { expConfig } from './abi'
 
export function SendTip() {
  const { address } = useAccount()
  const creatorAddress = '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'
 
  const { data, isPending, sendCalls } = useSendCalls()
 
  const {
    isLoading: isConfirming,
    isSuccess: isConfirmed, 
  } = useWaitForCallsStatus({
    id: data?.id,
  })
 
  async function submit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const shared = {
      abi: expConfig.abi,
      to: expConfig.address,
    }
    const amount = Value.fromEther('1')
    sendCalls({
      calls: [
        {
          ...shared,
          args: [address!, amount],
          functionName: 'approve',
        },
        {
          ...shared,
          args: [address!, creatorAddress, amount],
          functionName: 'transferFrom',
        },
      ],
    })
  }
 
  if (isConfirmed) 
    return ( 
      <div> 
        <img alt="Creator Avatar" src="/creator.png" /> 
        <div>Tip sent!</div> 
      </div> 
    ) 
 
  return (
    <form onSubmit={submit}>
      <button disabled={isPending || isConfirming} type="submit">
        {isPending ? (
          'Check prompt'
        ) : isConfirming ? (
          'Tipping creator…'
        ) : (
          'Tip creator'
        )}
      </button>
    </form>
  )
}