diff --git a/src/lib/components/providers/token-provider.svelte b/src/lib/components/providers/token-provider.svelte index 2f33ef4a..b3ed610e 100644 --- a/src/lib/components/providers/token-provider.svelte +++ b/src/lib/components/providers/token-provider.svelte @@ -8,7 +8,12 @@ import IntersectionObserver from "svelte-intersection-observer"; - export let address: string; + export let address: string | undefined = undefined; + + export let token: any | undefined = undefined; + + export let status: { isLoading: boolean; isError: boolean } | undefined = + undefined; let intersecting = false; const params = new URLSearchParams(window.location.search); @@ -16,39 +21,6 @@ const isMainnetValue = network !== "devnet"; const client = trpcWithQuery($page); - const token = client.token.createQuery([address, isMainnetValue], { - refetchOnMount: false, - refetchOnWindowFocus: false, - }); - - const accountInfo = client.accountInfo.createQuery( - [address, isMainnetValue], - { - refetchOnMount: false, - refetchOnWindowFocus: false, - } - ); - - const token2022Metadata: { - [key: string]: { - description: string; - image: string; - name: string; - }; - } = { - "2kMpEJCZL8vEDZe7YPLMCS9Y3WKSAMedXBn7xHPvsWvi": { - description: "Solana's 1 true moonshot. TW: @SolarMoonSol", - image: "https://gateway.ipfscdn.io/ipfs/bafkreifwdwgcv6fnh5icz3wkefokatsemsojck4hftsnuf4xcfjcvagsva/", - name: "SolarMoon (MOON)", - }, - CKfatsPMUf8SkiURsDXs7eK6GWb4Jsd6UDbs7twMCWxo: { - description: - "BonkEarn is the first of many experiments on the Token2022 standard, Bernzy sends his regards", - image: "https://i.imgur.com/nd9AVZ4.jpeg", - name: "BonkEarn (BERN)", - }, - }; - export const metadata: UITokenMetadata = { address: "", attributes: [], @@ -62,10 +34,18 @@ sellerFeeBasisPoints: 0, }; - const asset = client.asset.createQuery([address, isMainnetValue], { - refetchOnMount: false, - refetchOnWindowFocus: false, - }); + let asset: any | undefined; + + let deprecatedImage: any; + + if (address) { + asset = client.asset.createQuery([address, isMainnetValue], { + refetchOnMount: false, + refetchOnWindowFocus: false, + }); + } + + $: data = $asset?.data || token; let element: HTMLDivElement; @@ -73,8 +53,7 @@ metadata.name = "SOL"; metadata.image = "/media/tokens/solana.png"; metadata.address = SOL; - } else if ($asset?.data?.compressed) { - const data = $asset?.data; + } else if (data?.compressed) { metadata.address = data?.address || ""; metadata.attributes = data?.attributes || []; metadata.creators = data?.creators || []; @@ -94,45 +73,37 @@ metadata.tree = data?.tree || ""; metadata.seq = data?.seq || 0; metadata.leafId = data?.leafId || 0; - } - // TODO Property 'program' does not exist on type 'Buffer | ParsedAccountData'. - // @ts-ignore - else if ($accountInfo?.data?.value?.data?.program === "spl-token-2022") { - // const data = $accountInfo?.data?.value; - const data = token2022Metadata[address]; - metadata.name = data.name || ""; - metadata.description = data.description || ""; - metadata.image = data.image || ""; - metadata.address = address || ""; - } else { - // Kicks off the query - const data = $token?.data?.length ? $token.data[0] : {}; - metadata.address = data?.account; - metadata.attributes = data?.offChainMetadata?.metadata?.attributes; - metadata.sellerFeeBasisPoints = - data?.onChainMetadata?.metadata?.data?.sellerFeeBasisPoints || 0; - metadata.creators = data?.onChainMetadata?.metadata?.data?.creators; - metadata.description = data?.offChainMetadata?.metadata?.description; - metadata.collectionKey = - data?.onChainMetadata?.metadata?.collection?.key; + } else if (data?.compression.compressed == false) { + metadata.address = data?.id; + // metadata.attributes = data?.offChainMetadata?.metadata?.attributes; + metadata.sellerFeeBasisPoints = data?.royalty.basis_points || 0; + metadata.creators = data?.creators; + metadata.description = data?.content.metadata?.description; + // metadata.collectionKey = + // data?.onChainMetadata?.metadata?.collection?.key; metadata.image = - data?.offChainMetadata?.metadata?.image || - data?.onChainMetadata?.metadata?.data.image || - data?.legacyMetadata?.logoURI; - metadata.name = - data?.offChainMetadata?.metadata?.name || - data?.legacyMetadata?.name || - data?.onChainMetadata?.metadata?.data.name; - metadata.files = data?.offChainMetadata?.metadata?.properties?.files; - // Checking all files to see if a video exists - metadata.video_uri = - data?.offChainMetadata?.metadata?.properties?.files?.find( - (file: any) => file.type.startsWith("video/") + data?.content.links?.image || + data?.content.files?.find((file: any) => + file.mime?.startsWith("image/") )?.uri; + metadata.name = data?.content.metadata?.name; + metadata.files = data?.content.files; + // Checking all files to see if a video exists + metadata.video_uri = data?.content.files?.find((file: any) => + file.mime?.startsWith("video/") + )?.uri; + } + + $: if (data?.id && !metadata.image) { + deprecatedImage = client.deprecatedImage.createQuery(data?.id); + + metadata.image = $deprecatedImage?.data; } - $: tokenIsLoading = address !== SOL && $token.isLoading; - $: tokenFailed = $token.isError; + $: tokenIsLoading = + (address !== SOL && $asset?.isLoading) || + (address !== SOL && status?.isLoading); + $: tokenFailed = $asset?.isError || status?.isError; // This could be better $: isNFT = metadata?.attributes && metadata?.attributes?.length > 0; diff --git a/src/lib/trpc/router.ts b/src/lib/trpc/router.ts index 6bc632d2..eaeaf432 100644 --- a/src/lib/trpc/router.ts +++ b/src/lib/trpc/router.ts @@ -8,10 +8,11 @@ import { asset } from "$lib/trpc/routes/asset"; import { balances } from "$lib/trpc/routes/balances"; import { concurrentMerkleTree } from "$lib/trpc/routes/concurrent-merkle-tree"; import { currentSlot } from "$lib/trpc/routes/current-slot"; +import { deprecatedImage } from "./routes/deprecated-image"; import { price } from "$lib/trpc/routes/price"; import { rawTransaction } from "$lib/trpc/routes/raw-transaction"; +import { searchAssets } from "./routes/search-assets"; import { token } from "$lib/trpc/routes/token"; -import { token2022 } from "$lib/trpc/routes/token2022"; import { tps } from "$lib/trpc/routes/tps"; import { transaction } from "$lib/trpc/routes/transaction"; import { transactions } from "$lib/trpc/routes/transactions"; @@ -31,10 +32,11 @@ export const router = t.router({ cnftTransactions, concurrentMerkleTree, currentSlot, + deprecatedImage, price, rawTransaction, + searchAssets, token, - token2022, tps, transaction, transactions, diff --git a/src/lib/trpc/routes/asset.ts b/src/lib/trpc/routes/asset.ts index 674ef4f5..c3b58feb 100644 --- a/src/lib/trpc/routes/asset.ts +++ b/src/lib/trpc/routes/asset.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { HELIUS_API_KEY } from "$env/static/private"; import { getRPCUrl } from "$lib/util/get-rpc-url"; +import type { UITokenMetadata } from "$lib/types"; // TODO: add output validation once this merges with the token endpoint export const asset = t.procedure @@ -18,7 +19,12 @@ export const asset = t.procedure id: "asset", jsonrpc: "2.0", method: "getAsset", - params: [asset], + params: { + displayOptions: { + showFungible: true, + }, + id: asset, + }, }), headers: { "Content-Type": "application/json", @@ -27,27 +33,7 @@ export const asset = t.procedure }); const data = await response.json(); - let metadata = { - address: "", - assetHash: "", - attributes: [], - collectionKey: "", - compressed: false, - creatorHash: "", - creators: [], - dataHash: "", - delegate: "", - description: "", - frozen: false, - image: "", - leafId: 0, - mutable: false, - name: "", - owner: "", - sellerFeeBasisPoints: 0, - seq: 0, - tree: "", - }; + let metadata: UITokenMetadata | undefined; if (data?.result?.compression?.compressed === true) { const assetData = await fetch(data.result.content.json_uri); @@ -78,5 +64,5 @@ export const asset = t.procedure tree: data?.result?.compression?.tree, }; } - return metadata; + return metadata ?? data?.result; }); diff --git a/src/lib/trpc/routes/deprecated-image.ts b/src/lib/trpc/routes/deprecated-image.ts new file mode 100644 index 00000000..e7a3186d --- /dev/null +++ b/src/lib/trpc/routes/deprecated-image.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +import { t } from "$lib/trpc/t"; +import { getAPIUrl } from "$lib/util/get-api-url"; +import { HELIUS_API_KEY } from "$env/static/private"; + +export const deprecatedImage = t.procedure + .input(z.string()) + .query(async ({ input: account }) => { + const response = await fetch( + `https://github.com/solana-labs/token-list/blob/main/assets/mainnet/${account}/logo.png?raw=true` + ); + + const contentType = response.headers.get("content-type"); + + if (contentType?.includes("image/png")) { + return `https://github.com/solana-labs/token-list/blob/main/assets/mainnet/${account}/logo.png?raw=true`; + } else { + const url = getAPIUrl( + `/v0/token-metadata/?api-key=${HELIUS_API_KEY}`, + true + ); + const response = await fetch(url, { + body: JSON.stringify({ + includeOffChain: true, + mintAccounts: [account], + }), + method: "POST", + }); + const json = await response.json(); + return json[0]?.legacyMetadata?.logoURI ?? ""; + } + }); diff --git a/src/lib/trpc/routes/search-assets.ts b/src/lib/trpc/routes/search-assets.ts new file mode 100644 index 00000000..8c5b6a83 --- /dev/null +++ b/src/lib/trpc/routes/search-assets.ts @@ -0,0 +1,54 @@ +import { t } from "$lib/trpc/t"; + +import { z } from "zod"; + +import { HELIUS_API_KEY } from "$env/static/private"; +import { getRPCUrl } from "$lib/util/get-rpc-url"; + +export const searchAssets = t.procedure + .input( + z.object({ + account: z.string(), + cursor: z.number().optional(), + isMainnet: z.boolean(), + nativeBalance: z.boolean().optional(), + tokenType: z.string().optional(), + }) + ) + .query(async ({ input }) => { + const { + account, + cursor = 1, + isMainnet, + tokenType = "all", + nativeBalance = false, + } = input; + + const url = getRPCUrl(`/?api-key=${HELIUS_API_KEY}`, isMainnet); + + const response = await fetch(url, { + body: JSON.stringify({ + id: "my-id", + jsonrpc: "2.0", + method: "searchAssets", + params: { + displayOptions: { + showFungible: true, + showNativeBalance: nativeBalance, + }, + limit: 1000, + ownerAddress: account, + page: cursor, + sortBy: { sortBy: "id", sortDirection: "asc" }, + tokenType, + }, + }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + const { result } = await response.json(); + + return result; + }); diff --git a/src/lib/trpc/routes/token2022.ts b/src/lib/trpc/routes/token2022.ts deleted file mode 100644 index 6c542864..00000000 --- a/src/lib/trpc/routes/token2022.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { t } from "$lib/trpc/t"; -import { getRPCUrl } from "$lib/util/get-rpc-url"; -import { PublicKey } from "@solana/web3.js"; -import { z } from "zod"; - -import { HELIUS_API_KEY } from "$env/static/private"; - -export const token2022 = t.procedure - .input(z.tuple([z.string(), z.boolean()])) - .query(async ({ input }) => { - const [wallet, isMainnet] = input; - const TOKEN_2022_PROGRAM_ID = new PublicKey( - "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" - ); - - const walletPublicKey = new PublicKey(wallet); - const url = getRPCUrl(`?api-key=${HELIUS_API_KEY}`, isMainnet); - const response = await fetch(url, { - body: JSON.stringify({ - id: "accounts", - jsonrpc: "2.0", - method: "getTokenAccountsByOwner", - params: [ - walletPublicKey, - { - programId: TOKEN_2022_PROGRAM_ID, - }, - { - commitment: "confirmed", - encoding: "jsonParsed", - }, - ], - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }).then((res) => res.json()); - - type Accounts = { - amount: number; - mint: string; - }; - - // TODO: any type on this - const accounts: Accounts[] = response?.result?.value?.map( - (account: any) => { - return { - amount: account.account.data.parsed.info.tokenAmount - .uiAmount, - mint: account.account.data.parsed.info.mint, - }; - } - ); - - return accounts; - }); diff --git a/src/routes/account/[account]/tokens/+page.svelte b/src/routes/account/[account]/tokens/+page.svelte index cb5b3a33..36651935 100644 --- a/src/routes/account/[account]/tokens/+page.svelte +++ b/src/routes/account/[account]/tokens/+page.svelte @@ -15,19 +15,15 @@ const params = new URLSearchParams(window.location.search); const network = params.get("network"); const isMainnetValue = network !== "devnet"; - const balances = client.balances.createQuery([account, isMainnetValue]); - const token2022 = client.token2022.createQuery([account, isMainnetValue]); + const tokens = client.searchAssets.createQuery({ + account, + isMainnet: isMainnetValue, + nativeBalance: true, + tokenType: "fungible", + }); const sol = client.price.createQuery(SOL); - - $: sorted = $balances?.data?.tokens - // @ts-ignore - ?.filter(({ decimals, amount }) => decimals && amount) - // @ts-ignore - .sort(({ amount: a, decimals: ad }, { amount: b, decimals: bd }) => - a / 10 ** ad < b / 10 ** bd ? 1 : -1 - );
@@ -49,10 +45,11 @@

SOL

- {#if $balances.data?.nativeBalance} + {#if $tokens.data?.nativeBalance.lamports}

{( - $balances.data?.nativeBalance / LAMPORTS_PER_SOL + $tokens.data?.nativeBalance.lamports / + LAMPORTS_PER_SOL ).toLocaleString()}

{/if} @@ -60,7 +57,7 @@

{#if $sol.data} {formatMoney( - ($sol.data * $balances.data?.nativeBalance) / + ($sol.data * $tokens.data?.nativeBalance.lamports) / LAMPORTS_PER_SOL )} {/if} @@ -69,59 +66,20 @@

- {#if $token2022.data} - {#each $token2022.data as token} - - -
- -
-
-
-
-

- {metadata.name || ""} -

-
-
-

- {token.amount.toLocaleString()} -

- -
-
-
- - {/each} - {/if} - {#if sorted} - {#each sorted as token (token.mint)} - {#if token.decimals > 0 && token.mint !== SOL} + {#if $tokens.data} + {#each $tokens.data.items as token} + {#if token.token_info.decimals > 0 && token.id !== SOL}
@@ -141,15 +99,17 @@

{( - token.amount / - 10 ** token.decimals + token.token_info.balance / + 10 ** token.token_info.decimals ).toLocaleString()}

- {#if metadata.price} + {#if token.token_info.price_info} {formatMoney( - (metadata.price * token.amount) / - 10 ** token.decimals + (token.token_info?.price_info + ?.price_per_token * + token.token_info.balance) / + 10 ** token.token_info.decimals )} {/if}