1- import { createBaseAccountSDK } from '@base-org/account' ;
2- import { Box , Button } from '@chakra-ui/react' ;
1+ import type { TokenPaymentSuccess } from '@base-org/account' ;
2+ import { createBaseAccountSDK , payWithToken } from '@base-org/account' ;
3+ import { CheckCircleIcon , ExternalLinkIcon } from '@chakra-ui/icons' ;
4+ import { Box , Button , Flex , HStack , Icon , Link , Text , VStack } from '@chakra-ui/react' ;
35import { useCallback , useState } from 'react' ;
4- import { numberToHex } from 'viem' ;
5- import { baseSepolia } from 'viem/chains' ;
6+ import { formatUnits } from 'viem' ;
7+
8+ // Common token symbols and decimals
9+ const TOKEN_CONFIG : Record < string , { symbol : string ; decimals : number } > = {
10+ '0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4' : { symbol : 'USDC' , decimals : 6 } , // Base USDC
11+ '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' : { symbol : 'USDC' , decimals : 6 } , // Base mainnet USDC
12+ } ;
13+
14+ function getTokenInfo ( tokenAddress : string ) {
15+ const lowerAddress = tokenAddress . toLowerCase ( ) ;
16+ return TOKEN_CONFIG [ lowerAddress ] || { symbol : 'Token' , decimals : 18 } ;
17+ }
18+
19+ function stripChainPrefix ( txHash : string ) : string {
20+ // Remove chain prefix if present (e.g., "base:0x..." -> "0x...")
21+ return txHash . includes ( ':' ) ? txHash . split ( ':' ) [ 1 ] : txHash ;
22+ }
23+
24+ function getBlockExplorerUrl ( chainId : number , txHash : string ) : string {
25+ const hash = stripChainPrefix ( txHash ) ;
26+
27+ const explorers : Record < number , string > = {
28+ 8453 : 'https://basescan.org/tx' , // Base mainnet
29+ 84532 : 'https://sepolia.basescan.org/tx' , // Base Sepolia
30+ } ;
31+
32+ return `${ explorers [ chainId ] || 'https://basescan.org/tx' } /${ hash } ` ;
33+ }
634
735export function SendCalls ( {
836 sdk,
@@ -11,48 +39,45 @@ export function SendCalls({
1139 sdk : ReturnType < typeof createBaseAccountSDK > ;
1240 subAccountAddress : string ;
1341} ) {
14- const [ state , setState ] = useState < string > ( ) ;
15- const handleSendCalls = useCallback ( async ( ) => {
16- if ( ! sdk ) {
17- return ;
18- }
42+ const [ paymentResult , setPaymentResult ] = useState < TokenPaymentSuccess | null > ( null ) ;
43+ const [ isLoading , setIsLoading ] = useState ( false ) ;
44+ const [ error , setError ] = useState < string | null > ( null ) ;
45+
46+ const handlePayWithToken = useCallback ( async ( ) => {
47+ setIsLoading ( true ) ;
48+ setError ( null ) ;
49+ setPaymentResult ( null ) ;
1950
20- const provider = sdk . getProvider ( ) ;
2151 try {
22- const response = await provider . request ( {
23- method : 'wallet_sendCalls' ,
24- params : [
25- {
26- chainId : numberToHex ( baseSepolia . id ) ,
27- from : subAccountAddress ,
28- calls : [
29- {
30- to : '0x000000000000000000000000000000000000dead' ,
31- data : '0x' ,
32- value : '0x0' ,
33- } ,
34- ] ,
35- version : '1' ,
36- capabilities : {
37- paymasterService : {
38- url : 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O' ,
39- } ,
40- } ,
41- } ,
42- ] ,
52+ // Example payment with token
53+ const result = await payWithToken ( {
54+ amount : '100000000000000000000' , // 100 tokens (18 decimals)
55+ to : '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' ,
56+ token : '0xAC1Bd2486aAf3B5C0fc3Fd868558b082a531B2B4' , // USDC on Base Sepolia
57+ chainId : 8453 ,
58+ paymaster : {
59+ url : 'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O' ,
60+ } ,
4361 } ) ;
44- console . info ( 'response' , response ) ;
45- setState ( response as string ) ;
62+
63+ if ( result . success ) {
64+ setPaymentResult ( result ) ;
65+ }
4666 } catch ( e ) {
47- console . error ( 'error' , e ) ;
67+ console . error ( 'Payment error:' , e ) ;
68+ setError ( e instanceof Error ? e . message : 'Payment failed' ) ;
69+ } finally {
70+ setIsLoading ( false ) ;
4871 }
49- } , [ sdk , subAccountAddress ] ) ;
72+ } , [ ] ) ;
5073
5174 return (
52- < >
75+ < VStack spacing = { 4 } align = "stretch" w = "full" >
5376 < Button
5477 w = "full"
55- onClick = { handleSendCalls }
78+ onClick = { handlePayWithToken }
79+ isLoading = { isLoading }
80+ loadingText = "Processing..."
5681 bg = "blue.500"
5782 color = "white"
5883 border = "1px solid"
@@ -64,25 +89,115 @@ export function SendCalls({
6489 _hover : { bg : 'blue.700' , borderColor : 'blue.700' } ,
6590 } }
6691 >
67- Send Calls
92+ Pay with Token
6893 </ Button >
69- { state && (
94+
95+ { error && (
7096 < Box
71- as = "pre"
7297 w = "full"
73- p = { 2 }
74- bg = "gray .50"
75- borderRadius = "md "
98+ p = { 4 }
99+ bg = "red .50"
100+ borderRadius = "lg "
76101 border = "1px solid"
77- borderColor = "gray.300"
78- overflow = "auto"
79- whiteSpace = "pre-wrap"
80- color = "gray.800"
81- _dark = { { bg : 'gray.900' , borderColor : 'gray.700' , color : 'gray.200' } }
102+ borderColor = "red.200"
103+ _dark = { { bg : 'red.900' , borderColor : 'red.700' } }
82104 >
83- { JSON . stringify ( state , null , 2 ) }
105+ < HStack spacing = { 2 } >
106+ < Text color = "red.800" _dark = { { color : 'red.200' } } fontWeight = "medium" >
107+ ❌ Payment Failed
108+ </ Text >
109+ </ HStack >
110+ < Text color = "red.700" _dark = { { color : 'red.300' } } fontSize = "sm" mt = { 2 } >
111+ { error }
112+ </ Text >
113+ </ Box >
114+ ) }
115+
116+ { paymentResult && (
117+ < Box
118+ w = "full"
119+ p = { 6 }
120+ bg = "green.50"
121+ borderRadius = "lg"
122+ border = "1px solid"
123+ borderColor = "green.200"
124+ _dark = { { bg : 'green.900' , borderColor : 'green.700' } }
125+ >
126+ < HStack spacing = { 3 } mb = { 6 } >
127+ < Icon as = { CheckCircleIcon } boxSize = { 6 } color = "green.600" _dark = { { color : 'green.400' } } />
128+ < Text fontSize = "xl" fontWeight = "bold" color = "green.800" _dark = { { color : 'green.200' } } >
129+ Payment Successful!
130+ </ Text >
131+ </ HStack >
132+
133+ < VStack spacing = { 4 } align = "stretch" >
134+ { /* Amount */ }
135+ < Box >
136+ < Text fontSize = "sm" color = "gray.600" _dark = { { color : 'gray.400' } } mb = { 1 } >
137+ Amount
138+ </ Text >
139+ < Text fontSize = "lg" fontWeight = "semibold" color = "gray.900" _dark = { { color : 'white' } } >
140+ { formatUnits (
141+ BigInt ( paymentResult . tokenAmount ) ,
142+ getTokenInfo ( paymentResult . tokenAddress ) . decimals
143+ ) } { ' ' }
144+ { paymentResult . token || getTokenInfo ( paymentResult . tokenAddress ) . symbol }
145+ </ Text >
146+ </ Box >
147+
148+ { /* Recipient */ }
149+ < Box >
150+ < Text fontSize = "sm" color = "gray.600" _dark = { { color : 'gray.400' } } mb = { 1 } >
151+ Recipient
152+ </ Text >
153+ < Flex align = "center" gap = { 2 } >
154+ < Text
155+ fontSize = "md"
156+ fontFamily = "mono"
157+ color = "gray.700"
158+ _dark = { { color : 'gray.300' } }
159+ title = { paymentResult . to }
160+ >
161+ { paymentResult . to }
162+ </ Text >
163+ </ Flex >
164+ </ Box >
165+
166+ { /* Transaction ID */ }
167+ < Box >
168+ < Text fontSize = "sm" color = "gray.600" _dark = { { color : 'gray.400' } } mb = { 1 } >
169+ Transaction ID
170+ </ Text >
171+ < HStack spacing = { 2 } >
172+ < Text
173+ fontSize = "sm"
174+ fontFamily = "mono"
175+ color = "gray.700"
176+ _dark = { { color : 'gray.300' } }
177+ wordBreak = "break-all"
178+ >
179+ { stripChainPrefix ( paymentResult . id ) }
180+ </ Text >
181+ </ HStack >
182+ < Link
183+ href = { getBlockExplorerUrl ( paymentResult . chainId , paymentResult . id ) }
184+ isExternal
185+ color = "blue.600"
186+ _dark = { { color : 'blue.400' } }
187+ fontSize = "sm"
188+ mt = { 2 }
189+ display = "inline-flex"
190+ alignItems = "center"
191+ gap = { 1 }
192+ _hover = { { textDecoration : 'underline' } }
193+ >
194+ View on Block Explorer
195+ < ExternalLinkIcon />
196+ </ Link >
197+ </ Box >
198+ </ VStack >
84199 </ Box >
85200 ) }
86- </ >
201+ </ VStack >
87202 ) ;
88203}
0 commit comments