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 }