Permissions
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.
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.
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.
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.
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.
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.
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
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>
)
}