diff --git a/packages/bridge/src/components/TokenDisplay/index.tsx b/packages/bridge/src/components/TokenDisplay/index.tsx index ab55bc7..3d13d2e 100644 --- a/packages/bridge/src/components/TokenDisplay/index.tsx +++ b/packages/bridge/src/components/TokenDisplay/index.tsx @@ -6,10 +6,9 @@ import { useEthereum } from '../../contexts'; import { ASSET_CHAIN } from "../../models/bridge/constants"; import './style.less'; -export const TokenDisplay = ({ asset, chain, token }: { asset?: string, chain?: ASSET_CHAIN, token?: TokenInfo }) => { - +export const TokenDisplay = ({ asset, chain, token, logo }: { asset?: string, chain?: ASSET_CHAIN, token?: TokenInfo, logo?: string }) => { return
- +
; } diff --git a/packages/bridge/src/components/TokenDisplay/style.less b/packages/bridge/src/components/TokenDisplay/style.less index 8e11e91..d9bed6e 100644 --- a/packages/bridge/src/components/TokenDisplay/style.less +++ b/packages/bridge/src/components/TokenDisplay/style.less @@ -1,5 +1,8 @@ .token-chain-logo { - position: relative + position: relative; + width: 60px; + height: 60px; + display: flex; } .token-logo { @@ -11,12 +14,12 @@ } .chain-logo { - order: 1px solid hsla(0, 0%, 50.2%, 0.5); + border: 1px solid hsla(0, 0%, 50.2%, 0.5); border-radius: 20px; position: absolute; background: #fff; width: 30px; height: 30px; - bottom: 9px; - right: -5px; + bottom: 3px; + right: 3px; } diff --git a/packages/bridge/src/contexts/ethereum.tsx b/packages/bridge/src/contexts/ethereum.tsx index 45f8e82..e737fc3 100644 --- a/packages/bridge/src/contexts/ethereum.tsx +++ b/packages/bridge/src/contexts/ethereum.tsx @@ -79,7 +79,6 @@ export const EthereumProvider: FunctionComponent = ({children}) => { const signer = provider.getSigner(); signer.getAddress().then(account => setAccounts([account])); - setProvider(provider); } }, [connected]) diff --git a/packages/bridge/src/hooks/useWormholeAccounts.tsx b/packages/bridge/src/hooks/useWormholeAccounts.tsx index f5325ec..a5c4a4a 100644 --- a/packages/bridge/src/hooks/useWormholeAccounts.tsx +++ b/packages/bridge/src/hooks/useWormholeAccounts.tsx @@ -1,11 +1,11 @@ import {useCallback, useEffect, useRef, useState} from "react"; -import { useConnection, useConnectionConfig, MintParser, cache, getMultipleAccounts, ParsedAccount, TokenAccountParser, programIds} from "@oyster/common"; +import { useConnection, useConnectionConfig, MintParser, cache, getMultipleAccounts, ParsedAccount, TokenAccountParser, programIds, formatTokenAmount, fromLamports} from "@oyster/common"; import {WORMHOLE_PROGRAM_ID} from "../utils/ids"; import {ASSET_CHAIN} from "../utils/assets"; import { useEthereum } from "../contexts"; import { Connection, PublicKey } from "@solana/web3.js"; import { models } from "@oyster/common"; -import { MintInfo } from "@solana/spl-token"; +import { AccountInfo, MintInfo } from "@solana/spl-token"; import { bridgeAuthorityKey, wrappedAssetMintKey, WrappedMetaLayout } from './../models/bridge'; import bs58 from "bs58"; @@ -30,11 +30,11 @@ type WrappedAssetMeta = { const queryWrappedMetaAccounts = async ( + authorityKey: PublicKey, connection: Connection, setExternalAssets: (arr: WrappedAssetMeta[]) => void ) => { - // authority -> query for token accounts to get locked assets - let authorityKey = await bridgeAuthorityKey(programIds().wormhole.pubkey); + const filters = [ { @@ -81,6 +81,8 @@ const queryWrappedMetaAccounts = async ( mintKey: '', amount: 0, amountInUSD: 0, + // TODO: customize per chain + explorer: `https://etherscan.io/address/0x${assetAddress}` }); } } @@ -124,6 +126,7 @@ const queryWrappedMetaAccounts = async ( return; } asset.mint = cache.get(key); + asset.wrappedExplorer = `https://explorer.solana.com/address/${asset.mintKey}`; if(asset.mint) { @@ -139,17 +142,48 @@ const queryWrappedMetaAccounts = async ( asset.mint = cache.get(key); asset.amount = asset.mint?.info.supply.toNumber() || 0; - setExternalAssets([...assets.values()] - .sort((a, b) => a?.symbol?.localeCompare(b.symbol || '') || 0)); + setExternalAssets([...assets.values()]); }); } - setExternalAssets([...assets.values()] - .sort((a, b) => a?.symbol?.localeCompare(b.symbol || '') || 0)); + setExternalAssets([...assets.values()]); }); }; +const queryCustodyAccounts = async (authorityKey: PublicKey, connection: Connection) => { + debugger; + const tokenAccounts = await connection.getTokenAccountsByOwner( + authorityKey, + { + programId: programIds().token, + }).then(acc => acc.value.map(a => cache.add(a.pubkey, a.account, TokenAccountParser) as ParsedAccount)); + // query for mints + await getMultipleAccounts(connection, tokenAccounts.map(a => a.info.mint.toBase58()), 'single').then(({ keys, array }) => { + keys.forEach((key, index) => { + if(!array[index]) { + return; + } + + return cache.add(key, array[index], MintParser); + }) + }); + + return tokenAccounts.map(token => { + const mint = cache.get(token.info.mint) as ParsedAccount; + const asset = mint.pubkey.toBase58() + return { + address: asset, + chain: ASSET_CHAIN.Solana, + amount: fromLamports(token, mint.info), + mintKey: asset, + mint, + decimals: 9, + amountInUSD: 0, + explorer: `https://explorer.solana.com/address/${asset}`, + } as WrappedAssetMeta; + }) +}; export const useWormholeAccounts = () => { const connection = useConnection(); @@ -163,33 +197,35 @@ export const useWormholeAccounts = () => { const [externalAssets, setExternalAssets] = useState([]); const [amountInUSD, setAmountInUSD] = useState(0); - /// TODO: - /// assets that left Solana - // 1. getTokenAccountsByOwner with bridge PDA - // 2. get prices from serum ? - // 3. multiply account balances by - - /// assets locked from ETH - // 1. get asset address from proposal [x] - // 2. find the asset in the coingecko list or call abi? [] - // 3. build mint address using PDA [x] - // 4. query all mints [x] - // 5. multiply mint supply by asset price from coingecko [x] - // 6. aggregate all assets [x] - // 7. subscribe to program accounts useEffect(() => { setLoading(true); - queryWrappedMetaAccounts(connection, setExternalAssets).then(() => setLoading(false)); + let wormholeSubId = 0; + (async () => { + // authority -> query for token accounts to get locked assets + let authorityKey = await bridgeAuthorityKey(programIds().wormhole.pubkey); - const subId = connection.onProgramAccountChange(WORMHOLE_PROGRAM_ID, (info) => { - if (info.accountInfo.data.length === WrappedMetaLayout.span) { - // TODO: check if new account and update external assets - } - }); + // get all accounts that moved assets from solana to other chains + const custodyAccounts = await queryCustodyAccounts(authorityKey, connection); + + // query wrapped assets that were imported to solana from other chains + queryWrappedMetaAccounts(authorityKey, connection, (assets) => { + setExternalAssets([...custodyAccounts, ...assets] + .sort((a, b) => a?.symbol?.localeCompare(b.symbol || '') || 0)) + }).then(() => setLoading(false)); + + // TODO: listen to solana accounts for updates + + wormholeSubId = connection.onProgramAccountChange(WORMHOLE_PROGRAM_ID, (info) => { + if (info.accountInfo.data.length === WrappedMetaLayout.span) { + // TODO: check if new account and update external assets + } + }); + + })(); return () => { - connection.removeProgramAccountChangeListener(subId); + connection.removeProgramAccountChangeListener(wormholeSubId); }; }, [connection, setExternalAssets]); @@ -200,14 +236,13 @@ export const useWormholeAccounts = () => { } const addressToId = new Map(); - const idToAsset = new Map(); + const idToAsset = new Map(); const assetsToQueryNames: WrappedAssetMeta[] = []; const ids = externalAssets.map(asset => { // TODO: add different nets/clusters - asset.explorer = `https://etherscan.io/address/0x${asset.address}`; - asset.wrappedExplorer = `https://explorer.solana.com/address/${asset.mintKey}`; + let knownToken = tokenMap.get(asset.mintKey); if (knownToken) { @@ -227,7 +262,7 @@ export const useWormholeAccounts = () => { let coinInfo = coinList.get(asset.symbol.toLowerCase()); if(coinInfo) { - idToAsset.set(coinInfo.id, asset); + idToAsset.set(coinInfo.id, [...(idToAsset.get(coinInfo.id) || []), asset]); addressToId.set(asset.address, coinInfo.id); return coinInfo.id; } @@ -248,14 +283,17 @@ export const useWormholeAccounts = () => { let totalInUSD = 0; Object.keys(data).forEach(key => { - let asset = idToAsset.get(key); - if(!asset) { + let assets = idToAsset.get(key); + + if(!assets) { return; } - asset.price = data[key]?.usd || 1; - asset.amountInUSD = Math.round(asset.amount * (asset.price || 1) * 100) / 100; - totalInUSD += asset.amountInUSD; + assets.forEach(asset => { + asset.price = data[key]?.usd || 1; + asset.amountInUSD = Math.round(asset.amount * (asset.price || 1) * 100) / 100; + totalInUSD += asset.amountInUSD; + }); }); setAmountInUSD(totalInUSD); diff --git a/packages/bridge/src/views/home/index.tsx b/packages/bridge/src/views/home/index.tsx index 9305ea0..ab6001f 100644 --- a/packages/bridge/src/views/home/index.tsx +++ b/packages/bridge/src/views/home/index.tsx @@ -1,11 +1,12 @@ import { Table, Col, Row, Statistic, Button } from 'antd'; import React from 'react'; -import { GUTTER, LABELS } from '../../constants'; -import { formatNumber, formatTokenAmount, formatUSD, shortenAddress} from '@oyster/common'; +import { GUTTER } from '../../constants'; +import { formatNumber, formatUSD, shortenAddress} from '@oyster/common'; import './itemStyle.less'; import { Link } from 'react-router-dom'; import {useWormholeAccounts} from "../../hooks/useWormholeAccounts"; +import { TokenDisplay } from '../../components/TokenDisplay'; export const HomeView = () => { const { @@ -14,7 +15,6 @@ export const HomeView = () => { totalInUSD } = useWormholeAccounts(); - const columns = [ { title: 'Symbol', @@ -26,7 +26,7 @@ export const HomeView = () => { style: {}, }, children: ( - {record.logo && } {record.symbol} + {record.logo && } {record.symbol} ), }; }, diff --git a/packages/common/src/contexts/accounts.tsx b/packages/common/src/contexts/accounts.tsx index 92f1dee..1aee897 100644 --- a/packages/common/src/contexts/accounts.tsx +++ b/packages/common/src/contexts/accounts.tsx @@ -187,52 +187,7 @@ export const cache = { } return pubkey; - }, - queryMint: async (connection: Connection, pubKey: string | PublicKey) => { - let id: PublicKey; - if (typeof pubKey === 'string') { - id = new PublicKey(pubKey); - } else { - id = pubKey; - } - - const address = id.toBase58(); - let mint = mintCache.get(address); - if (mint) { - return mint; - } - - let query = pendingMintCalls.get(address); - if (query) { - return query; - } - - query = getMintInfo(connection, id).then((data) => { - pendingMintCalls.delete(address); - - mintCache.set(address, data); - return data; - }) as Promise; - pendingMintCalls.set(address, query as any); - - return query; - }, - getMint: (pubKey: string | PublicKey) => { - let key: string; - if (typeof pubKey !== 'string') { - key = pubKey.toBase58(); - } else { - key = pubKey; - } - - return mintCache.get(key); - }, - addMint: (pubKey: PublicKey, obj: AccountInfo) => { - const mint = deserializeMint(obj.data); - const id = pubKey.toBase58(); - mintCache.set(id, mint); - return mint; - }, + } }; export const useAccountsContext = () => {