Skip to content

Commit

Permalink
495 enable circle eurcs as spacewalk asset on the portal on pendulum (#…
Browse files Browse the repository at this point in the history
…525)

* Add cEURC.png icon

* Rename EURC.png to mEURC.png

* Refactor code

* Improve asset handling in dropdown selector

* Use issuer and code to compare stellar asset

* Remove log statement

* improve code readability 

* improve code readability

* extract mykobo logic into handleSpecialAsset function

* fix confusing EURC icons

---------

Co-authored-by: Kacper Szarkiewicz <[email protected]>
Co-authored-by: Kacper Szarkiewicz <[email protected]>
  • Loading branch information
3 people committed Jul 24, 2024
1 parent 830ac3b commit 301553b
Show file tree
Hide file tree
Showing 11 changed files with 112 additions and 64 deletions.
Binary file added src/assets/coins/cEURC.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
37 changes: 26 additions & 11 deletions src/components/Selector/AssetSelector/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StateUpdater, Dispatch } from 'preact/hooks';
import { getIcon } from '../../../shared/AssetIcons';
import { OrmlTraitsAssetRegistryAssetMetadata } from '../../../hooks/useBuyout/types';
import { assetDisplayName } from '../../../helpers/spacewalk';
import { stringifyStellarAsset } from '../../../helpers/stellar';

/* Types */
export type BlockchainAsset = Asset | OrmlTraitsAssetRegistryAssetMetadata;
Expand All @@ -12,22 +13,25 @@ export type AssetSelectorOnChange = Dispatch<StateUpdater<BlockchainAsset | unde
export function isStellarAsset(obj?: BlockchainAsset): obj is Asset {
return Boolean(obj && 'getCode' in obj && typeof obj.getCode === 'function');
}

export function areStellarAssets(objs?: BlockchainAsset[]): objs is Asset[] {
return objs !== undefined && objs.every((obj) => isStellarAsset(obj));
}

export function isOrmlAsset(obj?: BlockchainAsset): obj is OrmlTraitsAssetRegistryAssetMetadata {
return Boolean(obj && 'metadata' in obj && typeof obj.metadata === 'object' && 'symbol' in obj.metadata);
}

function areOrmlAssets(obj?: BlockchainAsset[]): obj is OrmlTraitsAssetRegistryAssetMetadata[] {
return Array.isArray(obj) && obj.every(isOrmlAsset);
}

export function getAssetIcon(asset?: BlockchainAsset): string {
function getAssetIcon(asset?: BlockchainAsset): string {
if (!asset) return '';
return isStellarAsset(asset) ? getIcon(asset?.code) : getIcon(asset.metadata.symbol);
return isStellarAsset(asset) ? getIcon(asset?.code, asset?.issuer) : getIcon(asset.metadata.symbol);
}

export function getAssetName(asset?: BlockchainAsset): string {
function getAssetName(asset?: BlockchainAsset): string {
if (!asset) return '';
return isStellarAsset(asset) ? asset?.code : asset.metadata.symbol;
}
Expand All @@ -44,7 +48,7 @@ const findAsset = (
};

function compareAsset(newItem: { id: string }) {
return (asset: Asset) => asset.getCode() === newItem.id;
return (asset: Asset) => stringifyStellarAsset(asset) === newItem.id;
}

function compareOrmlAsset(newItem: { id: string }) {
Expand Down Expand Up @@ -75,44 +79,55 @@ export function generateAssetSelectorItem(
) {
if (areStellarAssets(assets)) {
const formatAsset = (asset: Asset) => assetDisplayName(asset, assetPrefix, assetSuffix);
const getCode = (asset: Asset) => asset.getCode();
const getId = (asset: Asset) => stringifyStellarAsset(asset);

return generateAssetItems(
assets,
formatAsset as (asset: BlockchainAsset) => string,
getCode as (asset: BlockchainAsset) => string,
getId as (asset: BlockchainAsset) => string,
selectedAsset,
);
} else if (areOrmlAssets(assets)) {
const formatAsset = (asset: OrmlTraitsAssetRegistryAssetMetadata) => asset.metadata.symbol;
const getCode = (asset: OrmlTraitsAssetRegistryAssetMetadata) => asset.metadata.symbol;
const getId = (asset: OrmlTraitsAssetRegistryAssetMetadata) => asset.metadata.symbol;

return generateAssetItems(
assets,
formatAsset as (asset: BlockchainAsset) => string,
getCode as (asset: BlockchainAsset) => string,
getId as (asset: BlockchainAsset) => string,
selectedAsset,
);
}

return { formattedAssets: [], selectedAssetItem: undefined };
}

export interface AssetItem {
displayName: string;
id: string;
name: string;
icon?: string;
}

function generateAssetItems(
assets: BlockchainAsset[],
formatAssets: (asset: BlockchainAsset) => string,
getCode: (asset: BlockchainAsset) => string,
getId: (asset: BlockchainAsset) => string,
selectedAsset?: BlockchainAsset,
) {
const formattedAssets = assets.map((asset) => ({
displayName: formatAssets(asset),
id: getCode(asset),
id: getId(asset),
icon: getAssetIcon(asset),
name: getAssetName(asset),
}));

const selectedAssetItem = selectedAsset
? {
displayName: formatAssets(selectedAsset),
id: getCode(selectedAsset),
id: getId(selectedAsset),
icon: getAssetIcon(selectedAsset),
name: getAssetName(selectedAsset),
}
: undefined;

Expand Down
9 changes: 4 additions & 5 deletions src/components/Selector/AssetSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import {
AssetSelectorOnChange,
isStellarAsset,
generateAssetSelectorItem,
getAssetIcon,
onOrmlAssetOnChange,
onStellarAssetOnChange,
getAssetName,
AssetItem,
} from './helpers';

interface AssetSelectorProps {
Expand All @@ -27,8 +26,8 @@ interface AssetSelectorProps {
function AssetSelector(props: AssetSelectorProps): JSX.Element {
const { assets, selectedAsset, assetPrefix, assetSuffix, disabled = false } = props;

const [items, setItems] = useState<{ displayName: string; id: string }[]>([]);
const [selectedItem, setSelectedItem] = useState<{ displayName: string; id: string } | undefined>(undefined);
const [items, setItems] = useState<AssetItem[]>([]);
const [selectedItem, setSelectedItem] = useState<AssetItem | undefined>(undefined);

useEffect(() => {
const { formattedAssets, selectedAssetItem } = generateAssetSelectorItem(
Expand Down Expand Up @@ -60,7 +59,7 @@ function AssetSelector(props: AssetSelectorProps): JSX.Element {
type="button"
>
<span className="rounded-full bg-[rgba(0,0,0,0.15)] h-full mr-1 ">
<img src={getAssetIcon(selectedAsset)} alt={getAssetName(selectedAsset)} className="h-full w-auto " />
<img src={selectedItem?.icon} alt={selectedItem?.name} className="h-full w-auto " />
</span>
<strong className="font-bold">{selectedItem?.displayName}</strong>
{items.length > 1 ? <ChevronDownIcon className="w-4 h-4 inline ml-px" /> : <div className="px-1" />}
Expand Down
6 changes: 3 additions & 3 deletions src/components/Selector/DropdownSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Dropdown } from 'react-daisyui';
import { getIcon } from '../../shared/AssetIcons';
import { AssetItem } from './AssetSelector/helpers';

interface Props<T> {
items: T[];
Expand All @@ -8,7 +8,7 @@ interface Props<T> {
children: JSX.Element;
}

function DropdownSelector<T extends { id: unknown; displayName: string }>(props: Props<T>) {
function DropdownSelector<T extends AssetItem>(props: Props<T>) {
const { items, onChange, children } = props;
return (
<div className="flex flex-grow">
Expand All @@ -26,7 +26,7 @@ function DropdownSelector<T extends { id: unknown; displayName: string }>(props:
onChange(item);
}}
>
<img src={getIcon(item.displayName)} className="w-6" />
{item.icon && <img src={item.icon} className="w-6" alt={item?.name} />}
{item.displayName}
</Dropdown.Item>
))}
Expand Down
2 changes: 1 addition & 1 deletion src/components/nabla/Swap/From.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function From<FormFieldValues extends FieldValues, TFieldName extends Fie
type="button"
>
<span className="rounded-full bg-[rgba(0,0,0,0.15)] h-full p-px mr-1">
<img src={getIcon(fromToken?.symbol, pendulumIcon)} alt="Pendulum" className="h-full w-auto" />
<img src={getIcon(fromToken?.symbol)} alt={fromToken?.name} className="h-full w-auto" />
</span>
<strong className="font-bold">{fromToken?.symbol || 'Select'}</strong>
<ChevronDownIcon className="w-4 h-4 inline ml-px" />
Expand Down
2 changes: 1 addition & 1 deletion src/components/nabla/Swap/To.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function To({
type="button"
>
<span className="rounded-full bg-[rgba(0,0,0,0.15)] h-full p-px mr-1">
<img src={getIcon(toToken?.symbol, pendulumIcon)} alt="Pendulum" className="h-full w-auto" />
<img src={getIcon(toToken?.symbol)} alt={toToken?.name} className="h-full w-auto" />
</span>
<strong className="font-bold">{toToken?.symbol || 'Select'}</strong>
<ChevronDownIcon className="w-4 h-4 inline ml-px" />
Expand Down
2 changes: 1 addition & 1 deletion src/components/nabla/common/PoolSelectorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function PoolList({ swapPools, backstopPool, onSelect, selected }: PoolListProps
<Avatar
size={'xs' as AvatarProps['size']}
letters={pool.token.symbol}
src={getIcon(pool.token.symbol, pendulumIcon)}
src={getIcon(pool.token.symbol)}
shape="circle"
className="text-xs"
/>
Expand Down
6 changes: 6 additions & 0 deletions src/hooks/useBalances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { usePriceFetcher } from './usePriceFetcher';
import { useAssetRegistryMetadata } from './useAssetRegistryMetadata';
import { SpacewalkPrimitivesCurrencyId } from '@polkadot/types/lookup';
import { OrmlTraitsAssetRegistryAssetMetadata } from './useBuyout/types';
import { convertCurrencyToStellarAsset } from '../helpers/spacewalk';

function useBalances() {
const { walletAccount } = useGlobalState();
Expand Down Expand Up @@ -59,8 +60,13 @@ function useBalances() {
const amount = nativeToDecimal(free || '0', asset.metadata.decimals).toNumber();
const usdValue = price * amount;

// If the token is a Stellar CurrencyId we want to return it as well
// so we can display the correct icon in the portfolio
const stellarAsset = convertCurrencyToStellarAsset(asset.currencyId) || undefined;

return {
token,
asset: stellarAsset,
price,
amount,
usdValue,
Expand Down
36 changes: 20 additions & 16 deletions src/pages/dashboard/PortfolioColumns.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { ColumnDef } from '@tanstack/table-core';
import { Asset } from 'stellar-sdk';
import { getIcon } from '../../shared/AssetIcons';

export interface PortfolioAsset {
// token is the symbol of the asset
token: string;
// asset is also passed if it's a Stellar asset
asset?: Asset;
price: number;
amount: number;
usdValue?: number;
Expand All @@ -12,22 +16,22 @@ export const tokenColumn: ColumnDef<PortfolioAsset> = {
header: 'Token',
accessorKey: 'token',
enableMultiSort: true,
cell: ({ row }) => {
return (
<div className="flex flex-row">
<img
src={getIcon(row.original.token)}
className="mr-2"
style={{
objectFit: 'cover',
width: '32px',
height: '32px',
}}
/>
<div className="leading-8"> {row.original.token} </div>
</div>
);
},
cell: ({ row }) => (
<div className="flex flex-row">
<img
src={
row.original.asset ? getIcon(row.original.asset.code, row.original.asset.issuer) : getIcon(row.original.token)
}
className="mr-2"
style={{
objectFit: 'cover',
width: '32px',
height: '32px',
}}
/>
<div className="leading-8"> {row.original.token} </div>
</div>
),
};

export const priceColumn: ColumnDef<PortfolioAsset> = {
Expand Down
76 changes: 50 additions & 26 deletions src/shared/AssetIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import AMPE from '../assets/coins/AMPE.png';
import AUDD from '../assets/coins/AUDD.png';
import BRL from '../assets/coins/BRL.png';
import DOT from '../assets/coins/DOT.png';
import EURC from '../assets/coins/EURC.png';
import mEURC from '../assets/coins/mEURC.png';
import cEURC from '../assets/coins/cEURC.png';
import KSM from '../assets/coins/KSM.png';
import NGNC from '../assets/coins/NGNC.png';
import PEN from '../assets/coins/PEN.png';
Expand All @@ -14,33 +15,56 @@ import GLMR from '../assets/coins/GLMR.png';

import DefaultIcon from '../assets/coins/placeholder.png';

type IconMap = {
[key: string]: string;
// The public key of Mykobo's EURC issuer account
const MYKOBO_ISSUER = 'GAQRF3UGHBT6JYQZ7YSUYCIYWAF4T2SAA5237Q5LIQYJOHHFAWDXZ7NM';

// Maps all supported Stellar assets to icons. The EURC tokens are handled separately in `getAssetIcon()`
const stellarAssets = [
{ code: 'AUDD', icon: AUDD },
{ code: 'BRL', icon: BRL },
{ code: 'NGNC', icon: NGNC },
{ code: 'TZS', icon: TZS },
{ code: 'USDC', icon: USDC },
{ code: 'XLM', icon: XLM },
];

const polkadotAssets = [
{ code: 'PEN', icon: PEN },
{ code: 'DOT', icon: DOT },
{ code: 'AMPE', icon: AMPE },
{ code: 'KSM', icon: KSM },
{ code: 'USDT', icon: USDT },
{ code: 'GLMR', icon: GLMR },
];

const assets = [...stellarAssets, ...polkadotAssets];

const handleSpecialAsset = (assetCode: string, assetIssuer?: string) => {
// The EURC tokens are handled separately because they can be issued by multiple issuers
if (assetCode.includes('EURC')) {
if (assetIssuer === MYKOBO_ISSUER) {
return mEURC;
}
return cEURC;
}
};

const icons: IconMap = {
'AUDD.s': AUDD,
'BRL.s': BRL,
'EURC.s': EURC,
'NGNC.s': NGNC,
'TZS.s': TZS,
'USDC.s': USDC,
'XLM.s': XLM,
AMPE,
AUDD,
BRL,
DOT,
EURC,
KSM,
NGNC,
PEN,
TZS,
USDC,
USDT,
XLM,
GLMR,
const getAssetIcon = (assetCode: string, assetIssuer?: string) => {
const specialAsset = handleSpecialAsset(assetCode, assetIssuer);

if (specialAsset) {
return specialAsset;
}

const asset = assets.find((asset) => assetCode.includes(asset.code));

return asset?.icon || undefined;
};

export function getIcon(token: string | undefined, defaultIcon = DefaultIcon) {
return token && Object.keys(icons).includes(token) ? icons[token] : defaultIcon;
export function getIcon(token: string | undefined, issuer?: string, defaultIcon = DefaultIcon) {
if (!token) return defaultIcon;

const icon = getAssetIcon(token, issuer);

return icon || defaultIcon;
}

0 comments on commit 301553b

Please sign in to comment.