/ ui / src / utils / displayFunctions.ts
displayFunctions.ts
  1  import {
  2    stringifyRatioAsPercent,
  3    stringifyRatio,
  4    stringifyValue,
  5  } from '@agoric/ui-components';
  6  import { AssetKind, AmountMath } from '@agoric/ertp';
  7  import type { Brand, Amount, Ratio } from '@agoric/ertp/src/types';
  8  import {
  9    floorMultiplyBy,
 10    makeRatioFromAmounts,
 11  } from '@agoric/zoe/src/contractSupport';
 12  import type { BrandInfo } from 'store/app';
 13  
 14  export type PriceDescription = {
 15    amountIn: Amount<'nat'>;
 16    amountOut: Amount<'nat'>;
 17    timestamp?: { absValue: bigint };
 18  };
 19  
 20  export const getLogoForBrandPetname = (brandPetname: string) => {
 21    switch (brandPetname) {
 22      case 'ATOM':
 23        return 'ATOM.svg';
 24      case 'AXL':
 25        return 'AXL.svg';
 26      case 'BLD':
 27        return 'BLD.svg';
 28      case 'DAI_axl':
 29        return 'DAI_axl.png';
 30      case 'DAI_grv':
 31        return 'DAI_grv.png';
 32      case 'JUNO':
 33        return 'JUNO.svg';
 34      case 'IST':
 35        return 'IST.png';
 36      case 'OSMO':
 37        return 'OSMO.svg';
 38      case 'stATOM':
 39        return 'stATOM.svg';
 40      case 'stJUNO':
 41        return 'stJUNO.svg';
 42      case 'stOSMO':
 43        return 'stOSMO.svg';
 44      case 'USDC_axl':
 45        return 'USDC_axl.png';
 46      case 'USDC_grv':
 47        return 'USDC_grv.webp';
 48      case 'USDT_axl':
 49        return 'USDT_axl.png';
 50      case 'USDT_grv':
 51        return 'USDT_grv.webp';
 52      case 'WBTC_axl':
 53        return 'WBTC_axl.webp';
 54      case 'WETH_axl':
 55        return 'WETH_axl.webp';
 56      case 'WETH_grv':
 57        return 'WETH_grv.webp';
 58      default:
 59        return 'default.png';
 60    }
 61  };
 62  
 63  // We remove the "Ibc" prefix so e.g. "IbcATOM" becomes "ATOM".
 64  const wellKnownPetnames: Record<string, string> = {
 65    IbcATOM: 'ATOM',
 66  };
 67  
 68  export const displayPetname = (pn: string) =>
 69    wellKnownPetnames[pn] ?? (Array.isArray(pn) ? pn.join('.') : pn);
 70  
 71  export const makeDisplayFunctions = (brandToInfo: Map<Brand, BrandInfo>) => {
 72    const getDecimalPlaces = (brand: Brand) =>
 73      brandToInfo.get(brand)?.decimalPlaces;
 74  
 75    const getPetname = (brand?: Brand | null) =>
 76      (brand && brandToInfo.get(brand)?.petname) ?? '';
 77  
 78    const displayPercent = (ratio: Ratio, placesToShow: number) => {
 79      try {
 80        // This util function casts to Number, which can fail for very large
 81        // values.
 82        return stringifyRatioAsPercent(ratio, getDecimalPlaces, placesToShow);
 83      } catch {
 84        return '0';
 85      }
 86    };
 87  
 88    const displayBrandPetname = (brand?: Brand | null) => {
 89      return displayPetname(getPetname(brand));
 90    };
 91  
 92    const displayRatio = (ratio: Ratio, placesToShow: number) => {
 93      return stringifyRatio(ratio, getDecimalPlaces, placesToShow);
 94    };
 95  
 96    const displayAmount = (
 97      amount: Amount,
 98      placesToShow?: number,
 99      format?: 'usd' | 'locale',
100    ) => {
101      const decimalPlaces = getDecimalPlaces(amount.brand);
102      const parsed = stringifyValue(
103        amount.value,
104        AssetKind.NAT,
105        decimalPlaces,
106        placesToShow,
107      );
108  
109      if (format) {
110        const placesShown = parsed.split('.')[1]?.length ?? 0;
111        const usdOpts =
112          format === 'usd' ? { style: 'currency', currency: 'USD' } : {};
113  
114        return new Intl.NumberFormat(navigator.language, {
115          minimumFractionDigits: placesShown,
116          ...usdOpts,
117        }).format(Number(parsed));
118      }
119  
120      return parsed;
121    };
122  
123    const displayBrandIcon = (brand?: Brand | null) =>
124      getLogoForBrandPetname(getPetname(brand));
125  
126    const displayPrice = (price: PriceDescription, placesToShow?: number) => {
127      const { amountIn, amountOut } = price;
128      const { brand: brandIn } = amountIn;
129      const brandInDecimals = getDecimalPlaces(brandIn);
130      assert(brandInDecimals);
131  
132      const unitAmountOfBrandIn = AmountMath.make(
133        brandIn,
134        10n ** BigInt(brandInDecimals),
135      );
136  
137      const brandOutAmountPerUnitOfBrandIn = floorMultiplyBy(
138        unitAmountOfBrandIn,
139        makeRatioFromAmounts(amountOut, amountIn),
140      );
141  
142      return displayAmount(brandOutAmountPerUnitOfBrandIn, placesToShow, 'usd');
143    };
144  
145    const displayPriceTimestamp = (price: PriceDescription) => {
146      assert(price.timestamp, 'price missing timestamp');
147      return new Intl.DateTimeFormat(navigator.language, {
148        timeStyle: 'medium',
149        dateStyle: 'short',
150      }).format(new Date(Number(price.timestamp.absValue) * 1000));
151    };
152  
153    return {
154      displayPriceTimestamp,
155      displayPercent,
156      displayBrandPetname,
157      displayRatio,
158      displayAmount,
159      getDecimalPlaces,
160      displayBrandIcon,
161      displayPrice,
162    };
163  };
164  
165  export function toPercent(value: number, decimalPlaces: number): string {
166    return new Intl.NumberFormat(window?.navigator?.language || 'en-US', {
167      style: 'percent',
168      minimumFractionDigits: decimalPlaces,
169    }).format(value);
170  }
171  
172  export function toDollars(value: number, decimalPlaces: number): string {
173    return new Intl.NumberFormat(window?.navigator?.language || 'en-US', {
174      style: 'currency',
175      currency: 'USD',
176      minimumFractionDigits: decimalPlaces,
177    }).format(value);
178  }
179  
180  export function toFloat(value: number, decimalPlaces: number): string {
181    return new Intl.NumberFormat(window?.navigator?.language || 'en-US', {
182      style: 'decimal',
183      minimumFractionDigits: decimalPlaces,
184    }).format(value);
185  }
186  
187  export function parseLocaleFloat(stringNumber: string) {
188    const locale = window?.navigator?.language || 'en-US';
189    const thousandSeparator = Intl.NumberFormat(locale)
190      .format(11111)
191      .replace(/\p{Number}/gu, '');
192    const decimalSeparator = Intl.NumberFormat(locale)
193      .format(1.1)
194      .replace(/\p{Number}/gu, '');
195  
196    return String(
197      parseFloat(
198        stringNumber
199          .replace(new RegExp('\\' + thousandSeparator, 'g'), '')
200          .replace(new RegExp('\\' + decimalSeparator), '.'),
201      ),
202    );
203  }
204  
205  export function getSegments(
206    min: number,
207    max: number,
208    segments: number,
209    decimals = 3,
210  ) {
211    const factor = Math.pow(10, decimals);
212    const step = (max - min) / (segments - 1);
213    const marks = [];
214    for (let i = 0; i < segments; i++) {
215      const mark = min + i * step;
216      const rounded = Math.round(mark * factor) / factor;
217      marks.push(rounded);
218    }
219    return marks;
220  }