diff --git a/package-lock.json b/package-lock.json index ddb200a2..cde5ee6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@coral-xyz/anchor": "^0.29.0", "@lottiefiles/svelte-lottie-player": "^0.3.0", + "@nifty-oss/asset": "^0.3.0", "@onsol/tldparser": "^0.5.3", "@solana/spl-account-compression": "^0.1.8", "@solana/spl-token-registry": "^0.2.4574", @@ -1755,6 +1756,69 @@ "debug": "^4.3.4" } }, + "node_modules/@metaplex-foundation/umi": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi/-/umi-0.9.1.tgz", + "integrity": "sha512-IhHoOvp4vfO/++YL+78+iVuLM53+FDwUOZDYgH6lx0jYXyQ27BeaieeR5i+q3A9dz4KxQo5Nzc5aCA1109QGCQ==", + "peer": true, + "dependencies": { + "@metaplex-foundation/umi-options": "^0.8.9", + "@metaplex-foundation/umi-public-keys": "^0.8.9", + "@metaplex-foundation/umi-serializers": "^0.9.0" + } + }, + "node_modules/@metaplex-foundation/umi-options": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-options/-/umi-options-0.8.9.tgz", + "integrity": "sha512-jSQ61sZMPSAk/TXn8v8fPqtz3x8d0/blVZXLLbpVbo2/T5XobiI6/MfmlUosAjAUaQl6bHRF8aIIqZEFkJiy4A==", + "peer": true + }, + "node_modules/@metaplex-foundation/umi-public-keys": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-public-keys/-/umi-public-keys-0.8.9.tgz", + "integrity": "sha512-CxMzN7dgVGOq9OcNCJe2casKUpJ3RmTVoOvDFyeoTQuK+vkZ1YSSahbqC1iGuHEtKTLSjtWjKvUU6O7zWFTw3Q==", + "peer": true, + "dependencies": { + "@metaplex-foundation/umi-serializers-encodings": "^0.8.9" + } + }, + "node_modules/@metaplex-foundation/umi-serializers": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers/-/umi-serializers-0.9.0.tgz", + "integrity": "sha512-hAOW9Djl4w4ioKeR4erDZl5IG4iJdP0xA19ZomdaCbMhYAAmG/FEs5khh0uT2mq53/MnzWcXSUPoO8WBN4Q+Vg==", + "peer": true, + "dependencies": { + "@metaplex-foundation/umi-options": "^0.8.9", + "@metaplex-foundation/umi-public-keys": "^0.8.9", + "@metaplex-foundation/umi-serializers-core": "^0.8.9", + "@metaplex-foundation/umi-serializers-encodings": "^0.8.9", + "@metaplex-foundation/umi-serializers-numbers": "^0.8.9" + } + }, + "node_modules/@metaplex-foundation/umi-serializers-core": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-core/-/umi-serializers-core-0.8.9.tgz", + "integrity": "sha512-WT82tkiYJ0Qmscp7uTj1Hz6aWQPETwaKLAENAUN5DeWghkuBKtuxyBKVvEOuoXerJSdhiAk0e8DWA4cxcTTQ/w==", + "peer": true + }, + "node_modules/@metaplex-foundation/umi-serializers-encodings": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-encodings/-/umi-serializers-encodings-0.8.9.tgz", + "integrity": "sha512-N3VWLDTJ0bzzMKcJDL08U3FaqRmwlN79FyE4BHj6bbAaJ9LEHjDQ9RJijZyWqTm0jE7I750fU7Ow5EZL38Xi6Q==", + "peer": true, + "dependencies": { + "@metaplex-foundation/umi-serializers-core": "^0.8.9" + } + }, + "node_modules/@metaplex-foundation/umi-serializers-numbers": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-numbers/-/umi-serializers-numbers-0.8.9.tgz", + "integrity": "sha512-NtBf1fnVNQJHFQjLFzRu2i9GGnigb9hOm/Gfrk628d0q0tRJB7BOM3bs5C61VAs7kJs4yd+pDNVAERJkknQ7Lg==", + "peer": true, + "dependencies": { + "@metaplex-foundation/umi-serializers-core": "^0.8.9" + } + }, "node_modules/@ngraveio/bc-ur": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@ngraveio/bc-ur/-/bc-ur-1.1.6.tgz", @@ -1769,6 +1833,14 @@ "sha.js": "^2.4.11" } }, + "node_modules/@nifty-oss/asset": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nifty-oss/asset/-/asset-0.3.0.tgz", + "integrity": "sha512-2pAzMLYxCF2s/2NdKSxL/KwMhw/X6U2l2fHYpVkJbeym9h7kKe53mStiwXvX/hmFKGuWpfCySk8Qz6JUtUEUhA==", + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.1" + } + }, "node_modules/@noble/curves": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", diff --git a/package.json b/package.json index 72ef996c..a44df48d 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "dependencies": { "@coral-xyz/anchor": "^0.29.0", "@lottiefiles/svelte-lottie-player": "^0.3.0", + "@nifty-oss/asset": "^0.3.0", "@onsol/tldparser": "^0.5.3", "@solana/spl-account-compression": "^0.1.8", "@solana/spl-token-registry": "^0.2.4574", diff --git a/src/lib/components/providers/nifty-asset-provider.svelte b/src/lib/components/providers/nifty-asset-provider.svelte new file mode 100644 index 00000000..7b048179 --- /dev/null +++ b/src/lib/components/providers/nifty-asset-provider.svelte @@ -0,0 +1,98 @@ + + +
+ +
+ + {#if intersecting} + + {/if} + +
diff --git a/src/lib/trpc/router.ts b/src/lib/trpc/router.ts index eaeaf432..b88a4d17 100644 --- a/src/lib/trpc/router.ts +++ b/src/lib/trpc/router.ts @@ -9,6 +9,7 @@ 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 { niftyAsset } from "$lib/trpc/routes/nifty-asset"; import { price } from "$lib/trpc/routes/price"; import { rawTransaction } from "$lib/trpc/routes/raw-transaction"; import { searchAssets } from "./routes/search-assets"; @@ -33,6 +34,7 @@ export const router = t.router({ concurrentMerkleTree, currentSlot, deprecatedImage, + niftyAsset, price, rawTransaction, searchAssets, diff --git a/src/lib/trpc/routes/nifty-asset.ts b/src/lib/trpc/routes/nifty-asset.ts new file mode 100644 index 00000000..e9850c6f --- /dev/null +++ b/src/lib/trpc/routes/nifty-asset.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +import { t } from "$lib/trpc/t"; + +import { connect } from "$lib/xray"; +import { LAMPORTS_PER_SOL, PublicKey, Connection } from "@solana/web3.js"; +import { getRPCUrl } from "$lib/util/get-rpc-url"; + +import { HELIUS_API_KEY } from "$env/static/private"; +import { getAssetAccountDataSerializer } from "@nifty-oss/asset"; + +export const niftyAsset = t.procedure + .input(z.tuple([z.string(), z.boolean()])) + .query(async ({ input }) => { + const [address, isMainnet] = input; + const connection = new Connection( + getRPCUrl(`?api-key=${HELIUS_API_KEY}`, isMainnet), + "confirmed" + ); + + const pubKey = new PublicKey(address); + + const accountInfo = await connection.getAccountInfo(pubKey); + + if (accountInfo) { + return getAssetAccountDataSerializer().deserialize( + accountInfo.data + ); + } + + return null; + }); diff --git a/src/lib/types.ts b/src/lib/types.ts index abd76c52..bbe874ad 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,8 +5,11 @@ import type { EnrichedTransaction } from "helius-sdk"; import type { ProtonTransaction, ProtonTransactionAction } from "$lib/xray"; import type { IconPaths, modals } from "$lib/config"; + import type { SOL } from "$lib/xray"; +import type { Asset } from "@nifty-oss/asset"; + export * from "$lib/config"; export interface UIConfig { @@ -143,3 +146,6 @@ export type UISolAccountToken = { balanceInUSD: number; price: number; }; + +/** Used in the Nifty Asset pages. */ +export type UINiftyAsset = Asset & { json: any }; diff --git a/src/lib/util/stores/nifty-asset.ts b/src/lib/util/stores/nifty-asset.ts new file mode 100644 index 00000000..325f5033 --- /dev/null +++ b/src/lib/util/stores/nifty-asset.ts @@ -0,0 +1,4 @@ +import type { UINiftyAsset } from "$lib/types"; +import { writable } from "svelte/store"; + +export const niftyAssetStore = writable(null); diff --git a/src/lib/xray/lib/search.ts b/src/lib/xray/lib/search.ts index a3491b2c..e9eb4c61 100644 --- a/src/lib/xray/lib/search.ts +++ b/src/lib/xray/lib/search.ts @@ -8,6 +8,7 @@ import { TldParser } from "@onsol/tldparser"; import { browser } from "$app/environment"; import getJupiterTokens from "$lib/util/get-tokens"; +import { ASSET_PROGRAM_ID } from "@nifty-oss/asset"; export interface SearchResult { url: string; @@ -23,6 +24,7 @@ type SearchResultType = | "transaction" | "bonfida-domain" | "ans-domain" + | "nifty-asset" | null; const searchDefaults: SearchResult = { @@ -60,16 +62,19 @@ export const search = async ( const pubkey = new PublicKey(query); const account = await connection.getAccountInfo(pubkey); const program = account?.owner.toString(); - - const probablyToken = - program === "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" || - program === "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" || - account === null; - - let addressType!: "token" | "account"; - if (probablyToken) { - addressType = "token"; + let addressType!: "token" | "account" | "nifty-asset"; + + if (account) { + if ( + program === "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" || + program === "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" + ) { + addressType = "token"; + } else if (program === ASSET_PROGRAM_ID) { + addressType = "nifty-asset"; + } } + addressType ??= "account"; return { diff --git a/src/routes/account/[account]/assets/+page.svelte b/src/routes/account/[account]/assets/+page.svelte index 0d873cd1..5ed52719 100644 --- a/src/routes/account/[account]/assets/+page.svelte +++ b/src/routes/account/[account]/assets/+page.svelte @@ -42,7 +42,9 @@ {#if asset.burnt !== true} - {/if} + {/if} {/each} {/each}
diff --git a/src/routes/nifty-asset/[asset]/+page.svelte b/src/routes/nifty-asset/[asset]/+page.svelte new file mode 100644 index 00000000..9b81edbd --- /dev/null +++ b/src/routes/nifty-asset/[asset]/+page.svelte @@ -0,0 +1,893 @@ + + + + + + {#if loading} +
+ +
+ {:else} + + +
+ {#if mediaType} +
+
+ {#if mediaType === "video"} + +
+
+ {:else if mediaType === "image"} + +
+ asset media +
+ {:else if mediaType === "onchain"} + +
+ on-chain asset media +
+ {:else} + +
Loading...
+ {/if} +
+
+ {/if} + +
+
+ +
+ +
+

Authority

+
+

+ {asset.authority} +

+
+ +
+

Owner

+
+

+ {asset.owner} +

+
+ {#if asset.group} + +
+

Group

+
+

+ {asset.group} +

+
+ {:else} +
+
+

Group

+
+

None

+
+ {/if} + {#if asset.delegate} + +
+

Delegate

+ {#each asset.delegate.roles as role} +
+ {DelegateRole[role]} +
+ {/each} +
+

+ {asset.delegate} +

+
+ {:else} +
+
+

Delegate

+
+

None

+
+ {/if} +
+

+ State +

+

+ {asset.state === State.Locked + ? "Locked" + : "Unlocked"} +

+
+
+
+
+
+ + {#if asset.json || metadata} +
+
+ +

+ {#if asset.json && asset.json.description && asset.json.description.length > 0} + {asset.json.description} + {:else if metadata && metadata.description} + {metadata.description} + {/if} +

+
+
+
+ {/if} + + {#if royalties} +
+ +

+ {asset.name ?? "The"} creator(s) currently expect to + take {basisPointsToPercentage( + Number(royalties.basisPoints) + )} of every secondary sale on this piece. +

+
+
+ + {#if royalties.constraint.type !== "Empty"} +
+ + + +
+ {/if} + {/if} + + {#if creators && creators.creators.length > 0} +
+ +
+ {#each creators.creators as creator, idx} + +
+

+ CREATOR {idx + 1} +

+ +

+ {creator.share}% +

+
+
+

+ {creator.address} +

+
+ {/each} +
+
+
+ {/if} + + {#if asset.json} +
+ + + +
+ {/if} + + {#if hasExtensions} +
+ +

List of extensions attached to this asset.

+ {#if attributes} +
+ +
+ {#each attributes.traits as attribute, idx} +
+

+ {attribute.traitType} +

+

+ {attribute.value} +

+
+ {/each} +
+
+
+ {/if} + {#if blob} +
+ +
+ {#if mediaType !== "onchain" && mediaBlob} +
+ on-chain asset media +
+ {/if} +
+

+ Content-Type +

+

+ {blob.contentType} +

+
+
+

+ Data Size +

+

+ {blob.data.length} bytes +

+
+
+
+
+ {/if} + {#if creators} +
+ +
+ {#each creators.creators as creator, idx} + +
+

+ CREATOR {idx + 1} +

+ +

+ {creator.share}% +

+
+
+

+ {creator.address} +

+
+ {/each} +
+
+
+ {/if} + {#if grouping} +
+ +
+
+

+ Size +

+

+ {grouping.size} +

+
+
+

+ Maximum Size +

+

+ {grouping.maxSize} +

+
+ {#if grouping.delegate} + +
+

Delegate

+
+

+ {grouping.delegate} +

+
+ {:else} +
+
+

Delegate

+
+

None

+
+ {/if} +
+
+
+ {/if} + {#if links} +
+ +
+ {#each links.values as link} + +
+

{link.name}

+
+

+ {link.uri} +

+
+ {/each} +
+
+
+ {/if} + {#if manager} + + {/if} + {#if metadata} +
+ +
+
+

+ Symbol +

+

+ {metadata.symbol.length > 0 + ? metadata.symbol + : "None"} +

+
+
+

+ Description +

+

+ {metadata.description.length > 0 + ? metadata.description + : "None"} +

+
+ {#if metadata.uri.length > 0} + +
+

URI

+
+

+ {metadata.uri} +

+
+ {:else} +
+

+ URI +

+

None

+
+ {/if} +
+
+
+ {/if} + {#if proxy} +
+ + +
+

Program

+
+

+ {proxy.program} +

+
+
+
+

Seeds

+ +
+

{proxy.seeds}

+
+
+
+

Bump

+
+

{proxy.bump}

+
+ {#if proxy.authority} + +
+

Authority

+
+

+ {proxy.authority} +

+
+ {:else} +
+
+

Authority

+
+

None

+
+ {/if} +
+
+ {/if} + {#if royalties} +
+ +
+
+

Seller Fee

+
+

+ {royalties.basisPoints} +

+
+ {#if royalties.constraint.type !== "Empty"} + + {:else} +
+
+

Constraint

+
+

Empty

+
+ {/if} +
+
+ {/if} +
+
+ {/if} +
+ {/if} +
diff --git a/src/routes/nifty-asset/[asset]/_loader.svelte b/src/routes/nifty-asset/[asset]/_loader.svelte new file mode 100644 index 00000000..5fdd9d28 --- /dev/null +++ b/src/routes/nifty-asset/[asset]/_loader.svelte @@ -0,0 +1,87 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ {#each [1, 2, 3, 4, 5] as i} +
+
+
+
+ {/each} +
+
+
+
+
+
+
+
+
+ {#each [1, 2] as i} +
+
+
+
+ {/each} +
+
+
+
+