/ src / components / primitives / TokenIcon.tsx
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  }