From 473316c044cd011dc6c443b894e97c076cd1ac4d Mon Sep 17 00:00:00 2001 From: Joe Ellul-Turner Date: Sun, 10 Dec 2023 06:16:03 +0000 Subject: [PATCH 1/4] supports token2022 getAsset --- .../providers/token-provider.svelte | 93 +++++-------------- src/lib/trpc/routes/asset.ts | 32 ++----- .../account/[account]/tokens/+page.svelte | 2 +- 3 files changed, 33 insertions(+), 94 deletions(-) diff --git a/src/lib/components/providers/token-provider.svelte b/src/lib/components/providers/token-provider.svelte index 2f33ef4a..fd4ed63e 100644 --- a/src/lib/components/providers/token-provider.svelte +++ b/src/lib/components/providers/token-provider.svelte @@ -16,39 +16,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: [], @@ -94,45 +61,31 @@ 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 ($asset?.data?.result.compression.compressed == false) { + const data = $asset?.data.result; + + 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; } - $: tokenIsLoading = address !== SOL && $token.isLoading; - $: tokenFailed = $token.isError; + $: assetIsLoading = address !== SOL && $asset.isLoading; + $: assetFailed = $asset.isError; // This could be better $: isNFT = metadata?.attributes && metadata?.attributes?.length > 0; @@ -149,8 +102,8 @@ {#if intersecting} {/if} diff --git a/src/lib/trpc/routes/asset.ts b/src/lib/trpc/routes/asset.ts index 674ef4f5..885a348e 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; }); diff --git a/src/routes/account/[account]/tokens/+page.svelte b/src/routes/account/[account]/tokens/+page.svelte index cb5b3a33..f5300677 100644 --- a/src/routes/account/[account]/tokens/+page.svelte +++ b/src/routes/account/[account]/tokens/+page.svelte @@ -113,7 +113,7 @@ {/each} {/if} {#if sorted} - {#each sorted as token (token.mint)} + {#each sorted as token} {#if token.decimals > 0 && token.mint !== SOL} Date: Sun, 10 Dec 2023 07:25:33 +0000 Subject: [PATCH 2/4] reduced api calls --- .../providers/token-provider.svelte | 38 +++++--- src/lib/trpc/router.ts | 4 +- src/lib/trpc/routes/asset.ts | 2 +- src/lib/trpc/routes/search-assets.ts | 54 +++++++++++ src/lib/trpc/routes/token2022.ts | 57 ------------ .../account/[account]/tokens/+page.svelte | 90 ++++++------------- 6 files changed, 106 insertions(+), 139 deletions(-) create mode 100644 src/lib/trpc/routes/search-assets.ts delete mode 100644 src/lib/trpc/routes/token2022.ts diff --git a/src/lib/components/providers/token-provider.svelte b/src/lib/components/providers/token-provider.svelte index fd4ed63e..061c75dc 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); @@ -29,10 +34,16 @@ sellerFeeBasisPoints: 0, }; - const asset = client.asset.createQuery([address, isMainnetValue], { - refetchOnMount: false, - refetchOnWindowFocus: false, - }); + let asset: any | undefined; + + if (address) { + asset = client.asset.createQuery([address, isMainnetValue], { + refetchOnMount: false, + refetchOnWindowFocus: false, + }); + } + + $: data = $asset?.data || token; let element: HTMLDivElement; @@ -40,8 +51,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 || []; @@ -61,9 +71,7 @@ metadata.tree = data?.tree || ""; metadata.seq = data?.seq || 0; metadata.leafId = data?.leafId || 0; - } else if ($asset?.data?.result.compression.compressed == false) { - const data = $asset?.data.result; - + } else if (data?.compression.compressed == false) { metadata.address = data?.id; // metadata.attributes = data?.offChainMetadata?.metadata?.attributes; metadata.sellerFeeBasisPoints = data?.royalty.basis_points || 0; @@ -84,8 +92,10 @@ )?.uri; } - $: assetIsLoading = address !== SOL && $asset.isLoading; - $: assetFailed = $asset.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; @@ -102,8 +112,8 @@ {#if intersecting} {/if} diff --git a/src/lib/trpc/router.ts b/src/lib/trpc/router.ts index 6bc632d2..f52bc8ac 100644 --- a/src/lib/trpc/router.ts +++ b/src/lib/trpc/router.ts @@ -10,8 +10,8 @@ import { concurrentMerkleTree } from "$lib/trpc/routes/concurrent-merkle-tree"; import { currentSlot } from "$lib/trpc/routes/current-slot"; 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"; @@ -33,8 +33,8 @@ export const router = t.router({ currentSlot, 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 885a348e..c3b58feb 100644 --- a/src/lib/trpc/routes/asset.ts +++ b/src/lib/trpc/routes/asset.ts @@ -64,5 +64,5 @@ export const asset = t.procedure tree: data?.result?.compression?.tree, }; } - return metadata ?? data; + return metadata ?? data?.result; }); 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 f5300677..310432cc 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,54 +66,15 @@

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

- {metadata.name || ""} -

-
-
-

- {token.amount.toLocaleString()} -

- -
-
-
- - {/each} - {/if} - {#if sorted} - {#each sorted as token} - {#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}

{( - 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}

From 73470534b4a3f014ca68a37c3718dbb9faa0a2bc Mon Sep 17 00:00:00 2001 From: Joe Ellul-Turner Date: Sun, 10 Dec 2023 07:47:07 +0000 Subject: [PATCH 3/4] update href --- src/routes/account/[account]/tokens/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/account/[account]/tokens/+page.svelte b/src/routes/account/[account]/tokens/+page.svelte index 310432cc..36651935 100644 --- a/src/routes/account/[account]/tokens/+page.svelte +++ b/src/routes/account/[account]/tokens/+page.svelte @@ -79,7 +79,7 @@ >
From e183b0befde7c51af34121282c64b44331372556 Mon Sep 17 00:00:00 2001 From: Joe Ellul-Turner Date: Mon, 11 Dec 2023 19:16:22 +0000 Subject: [PATCH 4/4] support for old images --- .../providers/token-provider.svelte | 8 +++++ src/lib/trpc/router.ts | 2 ++ src/lib/trpc/routes/deprecated-image.ts | 33 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 src/lib/trpc/routes/deprecated-image.ts diff --git a/src/lib/components/providers/token-provider.svelte b/src/lib/components/providers/token-provider.svelte index 061c75dc..b3ed610e 100644 --- a/src/lib/components/providers/token-provider.svelte +++ b/src/lib/components/providers/token-provider.svelte @@ -36,6 +36,8 @@ let asset: any | undefined; + let deprecatedImage: any; + if (address) { asset = client.asset.createQuery([address, isMainnetValue], { refetchOnMount: false, @@ -92,6 +94,12 @@ )?.uri; } + $: if (data?.id && !metadata.image) { + deprecatedImage = client.deprecatedImage.createQuery(data?.id); + + metadata.image = $deprecatedImage?.data; + } + $: tokenIsLoading = (address !== SOL && $asset?.isLoading) || (address !== SOL && status?.isLoading); diff --git a/src/lib/trpc/router.ts b/src/lib/trpc/router.ts index f52bc8ac..eaeaf432 100644 --- a/src/lib/trpc/router.ts +++ b/src/lib/trpc/router.ts @@ -8,6 +8,7 @@ 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"; @@ -31,6 +32,7 @@ export const router = t.router({ cnftTransactions, concurrentMerkleTree, currentSlot, + deprecatedImage, price, rawTransaction, searchAssets, 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 ?? ""; + } + });