TokenIcon.tsx
1 import { Badge, Box, Icon, IconProps } from '@mui/material'; 2 import { forwardRef, useEffect, useRef, useState } from 'react'; 3 import LazyLoad from 'react-lazy-load'; 4 5 interface ATokenIconProps { 6 symbol?: string; 7 } 8 9 /** 10 * To save some bundle size we stopped base64 encoding & inlining svgs as base encoding increases size by up to 30% 11 * and most users will never need all token icons. 12 * The aToken icons have previously been separate icons also adding to bundle size. Now they are composed on the fly. 13 * When adding a token to metamask, you can either supply a url or a base64 encoded string. 14 * Supplying a url seems not very rational, but supplying a base64 for an external svg image that is composed with a react component is non trivial. 15 * Therefore the solution we came up with is: 16 * 1. rendering the svg component as an object 17 * 2. rendering the aToken ring as a react component 18 * 3. using js to manipulate the dome to have the object without the subdocument inside the react component 19 * 4. base64 encode the composed dom svg 20 * 21 * This component is probably hugely over engineered & unnecessary. 22 * I'm looking forward for the pr which evicts it. 23 */ 24 export function Base64Token({ 25 symbol, 26 onImageGenerated, 27 aToken, 28 }: { 29 symbol: string; 30 aToken?: boolean; 31 onImageGenerated: (base64: string) => void; 32 }) { 33 const ref = useRef<HTMLObjectElement>(null); 34 const aRef = useRef<SVGSVGElement>(null); 35 36 const [loading, setLoading] = useState(true); 37 useEffect(() => { 38 if (!loading && ref.current && ref.current?.contentDocument) { 39 if (aToken) { 40 // eslint-disable-next-line 41 const inner = ref.current?.contentDocument?.childNodes?.[0] as any; 42 const oldWidth = inner.getAttribute('width'); 43 const oldHeight = inner.getAttribute('height'); 44 const vb = inner.getAttribute('viewBox'); 45 inner.setAttribute('x', 25); 46 inner.setAttribute('width', 206); 47 inner.setAttribute('y', 25); 48 inner.setAttribute('height', 206); 49 if (!vb) { 50 inner.setAttribute('viewBox', `0 0 ${oldWidth} ${oldHeight}`); 51 } 52 53 aRef.current?.appendChild(inner); 54 const s = new XMLSerializer().serializeToString(aRef.current as unknown as Node); 55 56 onImageGenerated( 57 `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(s)))}` 58 ); 59 } else { 60 const s = new XMLSerializer().serializeToString(ref.current?.contentDocument); 61 onImageGenerated( 62 `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(s)))}` 63 ); 64 } 65 } 66 }, [loading, aToken]); 67 return ( 68 <div 69 style={{ 70 visibility: 'hidden', 71 height: 0, 72 width: 0, 73 overflow: 'hidden', 74 }} 75 > 76 <object 77 style={{ opacity: 1 }} 78 ref={ref} 79 id="svg" 80 data={`/icons/tokens/${symbol.toLowerCase()}.svg`} 81 onLoad={() => setLoading(false)} 82 /> 83 {aToken && <ATokenIcon ref={aRef} />} 84 </div> 85 ); 86 } 87 88 export const ATokenIcon = forwardRef<SVGSVGElement, ATokenIconProps>(({ symbol }, ref) => { 89 return ( 90 <svg 91 style={{ 92 position: 'absolute', 93 top: 0, 94 left: 0, 95 width: '100%', 96 height: '100%', 97 }} 98 ref={ref} 99 id="Group_30952" 100 width="256" 101 height="256" 102 viewBox="0 0 256 256" 103 > 104 <defs id="defs10"> 105 <linearGradient 106 id="linear-gradient" 107 x1=".843" 108 x2=".206" 109 y1=".135" 110 y2=".886" 111 gradientUnits="objectBoundingBox" 112 > 113 <stop offset="0" stopColor="#b6509e" id="stop2" /> 114 <stop offset="1" stopColor="#2ebac6" id="stop4" /> 115 </linearGradient> 116 <linearGradient id="linear-gradient-2" x1=".907" x2=".163" y1=".227" y2=".853" /> 117 </defs> 118 <g id="Group_29109"> 119 <path 120 id="Subtraction_108" 121 fill="url(#linear-gradient)" 122 d="M128 256a128.976 128.976 0 0 1-25.8-2.6 127.309 127.309 0 0 1-45.77-19.261 128.366 128.366 0 0 1-46.375-56.315A127.357 127.357 0 0 1 2.6 153.8a129.251 129.251 0 0 1 0-51.593 127.31 127.31 0 0 1 19.26-45.77 128.372 128.372 0 0 1 56.317-46.378A127.33 127.33 0 0 1 102.2 2.6a129.244 129.244 0 0 1 51.593 0 127.308 127.308 0 0 1 45.77 19.26 128.367 128.367 0 0 1 46.375 56.316A127.343 127.343 0 0 1 253.4 102.2a129.248 129.248 0 0 1 0 51.593 127.3 127.3 0 0 1-19.26 45.77 128.382 128.382 0 0 1-56.316 46.375A127.4 127.4 0 0 1 153.8 253.4 128.977 128.977 0 0 1 128 256zm0-242.287a115.145 115.145 0 0 0-23.033 2.322A113.657 113.657 0 0 0 64.1 33.232a114.622 114.622 0 0 0-41.4 50.283 113.7 113.7 0 0 0-6.659 21.452 115.4 115.4 0 0 0 0 46.065 113.66 113.66 0 0 0 17.2 40.866 114.627 114.627 0 0 0 50.282 41.407 113.75 113.75 0 0 0 21.453 6.658 115.381 115.381 0 0 0 46.065 0 113.609 113.609 0 0 0 40.866-17.2 114.622 114.622 0 0 0 41.393-50.278 113.741 113.741 0 0 0 6.659-21.453 115.4 115.4 0 0 0 0-46.065 113.662 113.662 0 0 0-17.2-40.865A114.619 114.619 0 0 0 172.485 22.7a113.74 113.74 0 0 0-21.453-6.659A115.145 115.145 0 0 0 128 13.714z" 123 /> 124 {symbol && ( 125 <image 126 x="25" 127 y="25" 128 href={`/icons/tokens/${symbol.toLowerCase()}.svg`} 129 width="206" 130 height="206" 131 /> 132 )} 133 </g> 134 </svg> 135 ); 136 }); 137 ATokenIcon.displayName = 'ATokenIcon'; 138 139 interface TokenIconProps extends IconProps { 140 symbol: string; 141 aToken?: boolean; 142 } 143 144 /** 145 * Renders a tokenIcon specified by symbol. 146 * TokenIcons are expected to be located at /public/icons/tokens and lowercase named <symbol>.svg 147 * @param param0 148 * @returns 149 */ 150 function SingleTokenIcon({ symbol, aToken, ...rest }: TokenIconProps) { 151 const [tokenSymbol, setTokenSymbol] = useState(symbol.toLowerCase()); 152 153 useEffect(() => { 154 setTokenSymbol(symbol.toLowerCase()); 155 }, [symbol]); 156 157 return ( 158 <Icon {...rest} sx={{ display: 'flex', position: 'relative', borderRadius: '50%', ...rest.sx }}> 159 {aToken ? ( 160 <ATokenIcon symbol={tokenSymbol} /> 161 ) : ( 162 // eslint-disable-next-line 163 <img 164 src={`/icons/tokens/${tokenSymbol}.svg`} 165 onError={() => setTokenSymbol('default')} 166 width="100%" 167 height="100%" 168 alt={`${symbol} icon`} 169 /> 170 )} 171 </Icon> 172 ); 173 } 174 175 /** 176 * Renders a tokenIcon specified by url. 177 * TokenIcons are expected to be non protocol related assets for swaps 178 * @param param0 179 * @returns 180 */ 181 182 interface ExternalTokenIconProps extends IconProps { 183 symbol: string; 184 logoURI?: string; 185 } 186 187 export function ExternalTokenIcon({ symbol, logoURI, ...rest }: ExternalTokenIconProps) { 188 const [tokenSymbol, setTokenSymbol] = useState(symbol.toLowerCase()); 189 return ( 190 <Icon {...rest} sx={{ display: 'flex', position: 'relative', borderRadius: '50%', ...rest.sx }}> 191 <LazyLoad> 192 <img 193 src={tokenSymbol === 'default' || !logoURI ? '/icons/tokens/default.svg' : logoURI} 194 width="100%" 195 height="100%" 196 alt={`${symbol} icon`} 197 onError={() => setTokenSymbol('default')} 198 /> 199 </LazyLoad> 200 </Icon> 201 ); 202 } 203 204 interface MultiTokenIconProps extends IconProps { 205 symbols: string[]; 206 badgeSymbol?: string; 207 aToken?: boolean; 208 } 209 210 export function MultiTokenIcon({ symbols, badgeSymbol, ...rest }: MultiTokenIconProps) { 211 if (!badgeSymbol) 212 return ( 213 <Box sx={{ display: 'inline-flex', position: 'relative' }}> 214 {symbols.map((symbol, ix) => ( 215 <SingleTokenIcon 216 {...rest} 217 key={symbol} 218 symbol={symbol} 219 sx={{ ml: ix === 0 ? 0 : `calc(-1 * 0.5em)`, ...rest.sx }} 220 /> 221 ))} 222 </Box> 223 ); 224 return ( 225 <Badge 226 badgeContent={ 227 <SingleTokenIcon symbol={badgeSymbol} sx={{ border: '1px solid #fff' }} fontSize="small" /> 228 } 229 sx={{ '.MuiBadge-anchorOriginTopRight': { top: 9 } }} 230 > 231 {symbols.map((symbol, ix) => ( 232 <SingleTokenIcon 233 {...rest} 234 key={symbol} 235 symbol={symbol} 236 sx={{ ml: ix === 0 ? 0 : 'calc(-1 * 0.5em)', ...rest.sx }} 237 /> 238 ))} 239 </Badge> 240 ); 241 } 242 243 export function TokenIcon({ symbol, ...rest }: TokenIconProps) { 244 const symbolChunks = symbol.split('_'); 245 if (symbolChunks.length > 1) { 246 const [badge, ...symbols] = symbolChunks; 247 return <MultiTokenIcon {...rest} symbols={symbols} badgeSymbol={'/pools/' + badge} />; 248 } 249 return <SingleTokenIcon symbol={symbol} {...rest} />; 250 }