Skip to content
Merged
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
23 changes: 23 additions & 0 deletions apps/react-example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { encodeFunctionData, erc20Abi, parseEther } from 'viem'
import {
useAccount,
useDisconnect,
useSendCalls,
useSendTransaction,
useSignMessage,
useSignTypedData,
Expand All @@ -26,6 +27,7 @@ function WalletPanel() {
error: signMessageError,
} = useSignMessage()
const { signTypedData } = useSignTypedData()
const { sendCalls } = useSendCalls()
const { pendingRequests } = usePendingRequest()

return (
Expand Down Expand Up @@ -140,6 +142,27 @@ function WalletPanel() {
})
}
/>
<Button
text="Batch Calls"
onClick={() =>
sendCalls({
calls: [
{
to: address!,
value: parseEther('0.01'),
},
{
to: '0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [address!, 1000000n],
}),
},
],
})
}
/>
</div>

{isSuccess && <p className="text-green-600 text-sm mt-2">tx: {data}</p>}
Expand Down
1 change: 1 addition & 0 deletions packages/react-kit/src/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { Request, RequestMethod } from './types.js'
const DEFAULT_SIGNING_PROMPT_METHODS: RequestMethod[] = [
'eth_sendTransaction',
'wallet_sendTransaction',
'wallet_sendCalls',
'personal_sign',
'eth_signTypedData_v4',
]
Expand Down
6 changes: 6 additions & 0 deletions packages/react-kit/src/signing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import type { Address, Hex } from 'viem'
import { usePendingRequest } from './hooks/usePendingRequest.js'
import { BatchCalls } from './pages/BatchCalls.js'
import { CollectionApproval } from './pages/CollectionApproval.js'
import { Erc20Approval } from './pages/Erc20Approval.js'
import { Erc20Transfer } from './pages/Erc20Transfer.js'
Expand Down Expand Up @@ -87,6 +88,11 @@ export function SignatureRequest() {
break
}

case 'wallet_sendCalls': {
const { calls } = pendingRequest.params[0]
return <BatchCalls calls={calls} confirm={confirm} reject={reject} />
}

case 'personal_sign': {
const [data, address] = pendingRequest.params
return (
Expand Down
236 changes: 236 additions & 0 deletions packages/react-kit/src/signing/pages/BatchCalls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import {
type Address,
erc20Abi,
formatEther,
formatUnits,
type Hex,
maxUint256,
} from 'viem'
import { useReadContract } from 'wagmi'
import type { BatchCall } from '../../types.js'
import { SigningActions } from '../components/SigningActions.js'
import {
decodeCollectionApproval,
isCollectionApproval,
} from '../utils/collectionApproval.js'
import { decodeErc20Approval, isErc20Approval } from '../utils/erc20Approval.js'
import { decodeErc20Transfer, isErc20Transfer } from '../utils/erc20Transfer.js'
import { isEthTransfer } from '../utils/ethTransfer.js'

interface BatchCallsProps {
calls: BatchCall[]
confirm: () => void
reject: () => void
}

function EthTransferItem({ to, value }: { to: Address; value: Hex }) {
return (
<div className="rounded-lg bg-gray-50 p-4 border border-gray-100">
<p className="text-sm font-medium text-gray-700">Send ETH</p>
<p className="text-lg font-bold text-gray-900">
{formatEther(BigInt(value))} ETH
</p>
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">To: </span>
<span className="font-mono break-all">{to}</span>
</div>
</div>
)
}

function Erc20TransferItem({
contract,
to,
amount,
}: {
contract: Address
to: Address
amount: bigint
}) {
const { data: symbol } = useReadContract({
address: contract,
abi: erc20Abi,
functionName: 'symbol',
})
const { data: decimals } = useReadContract({
address: contract,
abi: erc20Abi,
functionName: 'decimals',
})

const formatted =
decimals != null ? formatUnits(amount, decimals) : String(amount)
const label = symbol ?? contract

return (
<div className="rounded-lg bg-gray-50 p-4 border border-gray-100">
<p className="text-sm font-medium text-gray-700">Send {label}</p>
<p className="text-lg font-bold text-gray-900">
{formatted} {symbol ?? ''}
</p>
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">To: </span>
<span className="font-mono break-all">{to}</span>
</div>
</div>
)
}

function Erc20ApprovalItem({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these items meant to be components in different files?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure yet. Depends on how they are going to be presented in the designs. I think they may be just separate expandable cards - for now it makes sense keeping them here.

contract,
spender,
amount,
}: {
contract: Address
spender: Address
amount: bigint
}) {
const { data: symbol } = useReadContract({
address: contract,
abi: erc20Abi,
functionName: 'symbol',
})
const { data: decimals } = useReadContract({
address: contract,
abi: erc20Abi,
functionName: 'decimals',
})

const isUnlimited = amount === maxUint256
const formatted = isUnlimited
? 'Unlimited'
: decimals != null
? `${formatUnits(amount, decimals)} ${symbol ?? ''}`
: String(amount)

return (
<div className="rounded-lg bg-gray-50 p-4 border border-gray-100">
<p className="text-sm font-medium text-gray-700">
Approve {symbol ?? contract}
</p>
<p className="text-lg font-bold text-gray-900">{formatted}</p>
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">Spender: </span>
<span className="font-mono break-all">{spender}</span>
</div>
</div>
)
}

function CollectionApprovalItem({
contract,
operator,
approved,
}: {
contract: Address
operator: Address
approved: boolean
}) {
return (
<div className="rounded-lg bg-gray-50 p-4 border border-gray-100">
<p className="text-sm font-medium text-gray-700">
{approved ? 'Approve Collection' : 'Revoke Collection Approval'}
</p>
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">Contract: </span>
<span className="font-mono break-all">{contract}</span>
</div>
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">Operator: </span>
<span className="font-mono break-all">{operator}</span>
</div>
</div>
)
}

function UnknownCallItem({ call }: { call: BatchCall }) {
return (
<div className="rounded-lg bg-gray-50 p-4 border border-gray-100">
<p className="text-sm font-medium text-gray-700">Contract Call</p>
{call.to && (
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">To: </span>
<span className="font-mono break-all">{call.to}</span>
</div>
)}
{call.value && BigInt(call.value) > 0n && (
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">Value: </span>
<span>{formatEther(BigInt(call.value))} ETH</span>
</div>
)}
{call.data && (
<div className="mt-1 text-sm text-gray-500">
<span className="font-medium">Data: </span>
<span className="font-mono break-all">{call.data}</span>
</div>
)}
</div>
)
}

function CallItem({ call }: { call: BatchCall }) {
const tx = call as Parameters<typeof isEthTransfer>[0]

if (isEthTransfer(tx)) {
return <EthTransferItem to={call.to as Address} value={call.value as Hex} />
}

if (isErc20Transfer(tx)) {
const decoded = decodeErc20Transfer(tx)
if (decoded) {
return (
<Erc20TransferItem
contract={call.to as Address}
to={decoded.to}
amount={decoded.amount}
/>
)
}
}

if (isErc20Approval(tx)) {
const decoded = decodeErc20Approval(tx)
if (decoded) {
return (
<Erc20ApprovalItem
contract={call.to as Address}
spender={decoded.spender}
amount={decoded.amount}
/>
)
}
}

if (isCollectionApproval(tx)) {
const decoded = decodeCollectionApproval(tx)
if (decoded) {
return (
<CollectionApprovalItem
contract={call.to as Address}
operator={decoded.operator}
approved={decoded.approved}
/>
)
}
}

return <UnknownCallItem call={call} />
}

export function BatchCalls({ calls, confirm, reject }: BatchCallsProps) {
return (
<div className="flex flex-col gap-3">
<h3 className="text-lg font-semibold text-gray-900">
Batch Transaction ({calls.length}{' '}
{calls.length === 1 ? 'call' : 'calls'})
</h3>

{calls.map((call, i) => (
<CallItem key={`${call.to ?? 'unknown'}-${i}`} call={call} />
))}

<SigningActions confirm={confirm} reject={reject} />
</div>
)
}
19 changes: 18 additions & 1 deletion packages/react-kit/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { Hex, RpcTransactionRequest } from 'viem'
import type { Address, Hex, RpcTransactionRequest } from 'viem'

export type BatchCall = {
to?: Address
data?: Hex
value?: Hex
}

// todo: these should be moved to core package and exported from there
export type Request =
Expand All @@ -10,6 +16,17 @@ export type Request =
method: 'wallet_sendTransaction'
params: [transaction: RpcTransactionRequest]
}
| {
method: 'wallet_sendCalls'
params: [
{
version: string
chainId: Hex
from: Address
calls: BatchCall[]
},
]
}
| {
method: 'personal_sign'
params: [data: Hex, address: Hex]
Expand Down
Loading