[Explorer] Change to consume @metaplex/js (#20637)
* Removed duplicated Metaplex logic/schemas and instead consume @metaplex/js * Bumped @metaplex/js version to 1.2.0 Co-authored-by: Will Roeder <roederw@wills-mbp.lan>
This commit is contained in:
parent
70b2e5fef2
commit
05569caade
|
@ -12,6 +12,7 @@
|
||||||
"@bonfida/bot": "^0.5.3",
|
"@bonfida/bot": "^0.5.3",
|
||||||
"@cloudflare/stream-react": "^1.2.0",
|
"@cloudflare/stream-react": "^1.2.0",
|
||||||
"@metamask/jazzicon": "^2.0.0",
|
"@metamask/jazzicon": "^2.0.0",
|
||||||
|
"@metaplex/js": "1.2.0",
|
||||||
"@project-serum/serum": "^0.13.60",
|
"@project-serum/serum": "^0.13.60",
|
||||||
"@react-hook/debounce": "^4.0.0",
|
"@react-hook/debounce": "^4.0.0",
|
||||||
"@sentry/react": "^6.13.3",
|
"@sentry/react": "^6.13.3",
|
||||||
|
@ -3499,6 +3500,83 @@
|
||||||
"color-name": "^1.0.0"
|
"color-name": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@metaplex/js": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@metaplex/js/-/js-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-qbfie0Zwish72Ry3UdA5YVgNzLYqLm86EOsnBos9UNkTRr+Yb2/Xh/l5cgws6lUNpnTLm9c8PaLRmtRvuLD3tQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@solana/spl-token": "^0.1.8",
|
||||||
|
"@solana/web3.js": "^1.24.1",
|
||||||
|
"@types/bs58": "^4.0.1",
|
||||||
|
"axios": "^0.21.4",
|
||||||
|
"bn.js": "^5.2.0",
|
||||||
|
"borsh": "^0.4.0",
|
||||||
|
"bs58": "^4.0.1",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"crypto-hash": "^1.3.0",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@metaplex/js/node_modules/@solana/spl-token": {
|
||||||
|
"version": "0.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.8.tgz",
|
||||||
|
"integrity": "sha512-LZmYCKcPQDtJgecvWOgT/cnoIQPWjdH+QVyzPcFvyDUiT0DiRjZaam4aqNUyvchLFhzgunv3d9xOoyE34ofdoQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.10.5",
|
||||||
|
"@solana/web3.js": "^1.21.0",
|
||||||
|
"bn.js": "^5.1.0",
|
||||||
|
"buffer": "6.0.3",
|
||||||
|
"buffer-layout": "^1.2.0",
|
||||||
|
"dotenv": "10.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@metaplex/js/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@metaplex/js/node_modules/dotenv": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@metaplex/js/node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
|
||||||
|
@ -28902,6 +28980,62 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@metaplex/js": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@metaplex/js/-/js-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-qbfie0Zwish72Ry3UdA5YVgNzLYqLm86EOsnBos9UNkTRr+Yb2/Xh/l5cgws6lUNpnTLm9c8PaLRmtRvuLD3tQ==",
|
||||||
|
"requires": {
|
||||||
|
"@solana/spl-token": "^0.1.8",
|
||||||
|
"@solana/web3.js": "^1.24.1",
|
||||||
|
"@types/bs58": "^4.0.1",
|
||||||
|
"axios": "^0.21.4",
|
||||||
|
"bn.js": "^5.2.0",
|
||||||
|
"borsh": "^0.4.0",
|
||||||
|
"bs58": "^4.0.1",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"crypto-hash": "^1.3.0",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@solana/spl-token": {
|
||||||
|
"version": "0.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.8.tgz",
|
||||||
|
"integrity": "sha512-LZmYCKcPQDtJgecvWOgT/cnoIQPWjdH+QVyzPcFvyDUiT0DiRjZaam4aqNUyvchLFhzgunv3d9xOoyE34ofdoQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.10.5",
|
||||||
|
"@solana/web3.js": "^1.21.0",
|
||||||
|
"bn.js": "^5.1.0",
|
||||||
|
"buffer": "6.0.3",
|
||||||
|
"buffer-layout": "^1.2.0",
|
||||||
|
"dotenv": "10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"requires": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dotenv": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
|
||||||
|
},
|
||||||
|
"form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@nodelib/fs.scandir": {
|
"@nodelib/fs.scandir": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"@bonfida/bot": "^0.5.3",
|
"@bonfida/bot": "^0.5.3",
|
||||||
"@cloudflare/stream-react": "^1.2.0",
|
"@cloudflare/stream-react": "^1.2.0",
|
||||||
"@metamask/jazzicon": "^2.0.0",
|
"@metamask/jazzicon": "^2.0.0",
|
||||||
|
"@metaplex/js": "1.2.0",
|
||||||
"@project-serum/serum": "^0.13.60",
|
"@project-serum/serum": "^0.13.60",
|
||||||
"@react-hook/debounce": "^4.0.0",
|
"@react-hook/debounce": "^4.0.0",
|
||||||
"@sentry/react": "^6.13.3",
|
"@sentry/react": "^6.13.3",
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import "bootstrap/dist/js/bootstrap.min.js";
|
import "bootstrap/dist/js/bootstrap.min.js";
|
||||||
import { NFTData } from "providers/accounts";
|
import { NFTData } from "providers/accounts";
|
||||||
import { Creator } from "metaplex/classes";
|
import { Creator } from "@metaplex/js";
|
||||||
import { ArtContent } from "metaplex/Art/Art";
|
import { ArtContent } from "components/common/NFTArt";
|
||||||
import { InfoTooltip } from "components/common/InfoTooltip";
|
import { InfoTooltip } from "components/common/InfoTooltip";
|
||||||
import { EditionData } from "providers/accounts/utils/metadataHelpers";
|
|
||||||
import { clusterPath } from "utils/url";
|
import { clusterPath } from "utils/url";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { EditionInfo } from "providers/accounts/utils/getEditionInfo";
|
||||||
|
|
||||||
export function NFTHeader({
|
export function NFTHeader({
|
||||||
nftData,
|
nftData,
|
||||||
|
@ -29,7 +29,7 @@ export function NFTHeader({
|
||||||
? metadata.data.name
|
? metadata.data.name
|
||||||
: "No NFT name was found"}
|
: "No NFT name was found"}
|
||||||
</h2>
|
</h2>
|
||||||
{getEditionPill(nftData.editionData)}
|
{getEditionPill(nftData.editionInfo)}
|
||||||
</div>
|
</div>
|
||||||
<h4 className="header-pretitle ml-1 mt-1">
|
<h4 className="header-pretitle ml-1 mt-1">
|
||||||
{metadata.data.symbol !== ""
|
{metadata.data.symbol !== ""
|
||||||
|
@ -127,9 +127,9 @@ function getCreatorDropdownItems(creators: Creator[] | null) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEditionPill(editionData?: EditionData) {
|
function getEditionPill(editionInfo: EditionInfo) {
|
||||||
const masterEdition = editionData?.masterEdition;
|
const masterEdition = editionInfo.masterEdition;
|
||||||
const edition = editionData?.edition;
|
const edition = editionInfo.edition;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"d-inline-flex ml-2"}>
|
<div className={"d-inline-flex ml-2"}>
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { Copyable } from "components/common/Copyable";
|
||||||
import { CoingeckoStatus, useCoinGecko } from "utils/coingecko";
|
import { CoingeckoStatus, useCoinGecko } from "utils/coingecko";
|
||||||
import { displayTimestampWithoutDate } from "utils/date";
|
import { displayTimestampWithoutDate } from "utils/date";
|
||||||
import { LoadingCard } from "components/common/LoadingCard";
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
import { toPublicKey } from "metaplex/ids";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
|
||||||
const getEthAddress = (link?: string) => {
|
const getEthAddress = (link?: string) => {
|
||||||
let address = "";
|
let address = "";
|
||||||
|
@ -314,23 +314,23 @@ function NonFungibleTokenMintAccountCard({
|
||||||
<Address pubkey={account.pubkey} alignRight raw />
|
<Address pubkey={account.pubkey} alignRight raw />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{nftData?.editionData?.masterEdition?.maxSupply && (
|
{nftData.editionInfo.masterEdition?.maxSupply && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>Max Total Supply</td>
|
<td>Max Total Supply</td>
|
||||||
<td className="text-lg-right">
|
<td className="text-lg-right">
|
||||||
{nftData.editionData.masterEdition.maxSupply.toNumber() === 0
|
{nftData.editionInfo.masterEdition.maxSupply.toNumber() === 0
|
||||||
? 1
|
? 1
|
||||||
: nftData.editionData.masterEdition.maxSupply.toNumber()}
|
: nftData.editionInfo.masterEdition.maxSupply.toNumber()}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{nftData?.editionData?.masterEdition?.supply && (
|
{nftData?.editionInfo.masterEdition?.supply && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>Current Supply</td>
|
<td>Current Supply</td>
|
||||||
<td className="text-lg-right">
|
<td className="text-lg-right">
|
||||||
{nftData.editionData.masterEdition.supply.toNumber() === 0
|
{nftData.editionInfo.masterEdition.supply.toNumber() === 0
|
||||||
? 1
|
? 1
|
||||||
: nftData.editionData.masterEdition.supply.toNumber()}
|
: nftData.editionInfo.masterEdition.supply.toNumber()}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
@ -346,7 +346,7 @@ function NonFungibleTokenMintAccountCard({
|
||||||
<td>Update Authority</td>
|
<td>Update Authority</td>
|
||||||
<td className="text-lg-right">
|
<td className="text-lg-right">
|
||||||
<Address
|
<Address
|
||||||
pubkey={toPublicKey(nftData.metadata.updateAuthority)}
|
pubkey={new PublicKey(nftData.metadata.updateAuthority)}
|
||||||
alignRight
|
alignRight
|
||||||
link
|
link
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { MetadataCategory, MetadataFile } from "../types";
|
|
||||||
import { pubkeyToString } from "../utils";
|
|
||||||
import { useCachedImage, useExtendedArt } from "./useArt";
|
|
||||||
import { Stream, StreamPlayerApi } from "@cloudflare/stream-react";
|
import { Stream, StreamPlayerApi } from "@cloudflare/stream-react";
|
||||||
import { PublicKey } from "@solana/web3.js";
|
import { PublicKey } from "@solana/web3.js";
|
||||||
import { getLast } from "../utils";
|
import {
|
||||||
import { Metadata } from "metaplex/classes";
|
MetadataData,
|
||||||
|
MetadataJson,
|
||||||
|
MetaDataJsonCategory,
|
||||||
|
MetadataJsonFile,
|
||||||
|
} from "@metaplex/js";
|
||||||
import ContentLoader from "react-content-loader";
|
import ContentLoader from "react-content-loader";
|
||||||
import ErrorLogo from "img/logos-solana/dark-solana-logo.svg";
|
import ErrorLogo from "img/logos-solana/dark-solana-logo.svg";
|
||||||
|
import { getLast, pubkeyToString } from "utils";
|
||||||
|
|
||||||
const MAX_TIME_LOADING_IMAGE = 5000; /* 5 seconds */
|
const MAX_TIME_LOADING_IMAGE = 5000; /* 5 seconds */
|
||||||
|
|
||||||
|
@ -90,7 +92,7 @@ const VideoArtContent = ({
|
||||||
animationURL,
|
animationURL,
|
||||||
active,
|
active,
|
||||||
}: {
|
}: {
|
||||||
files?: (MetadataFile | string)[];
|
files?: (MetadataJsonFile | string)[];
|
||||||
uri?: string;
|
uri?: string;
|
||||||
animationURL?: string;
|
animationURL?: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
@ -168,7 +170,7 @@ const HTMLContent = ({
|
||||||
files,
|
files,
|
||||||
}: {
|
}: {
|
||||||
animationUrl?: string;
|
animationUrl?: string;
|
||||||
files?: (MetadataFile | string)[];
|
files?: (MetadataJsonFile | string)[];
|
||||||
}) => {
|
}) => {
|
||||||
const [loaded, setLoaded] = useState<boolean>(false);
|
const [loaded, setLoaded] = useState<boolean>(false);
|
||||||
const htmlURL =
|
const htmlURL =
|
||||||
|
@ -207,13 +209,13 @@ export const ArtContent = ({
|
||||||
animationURL,
|
animationURL,
|
||||||
files,
|
files,
|
||||||
}: {
|
}: {
|
||||||
metadata: Metadata;
|
metadata: MetadataData;
|
||||||
category?: MetadataCategory;
|
category?: MetaDataJsonCategory;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
pubkey?: PublicKey | string;
|
pubkey?: PublicKey | string;
|
||||||
uri?: string;
|
uri?: string;
|
||||||
animationURL?: string;
|
animationURL?: string;
|
||||||
files?: (MetadataFile | string)[];
|
files?: (MetadataJsonFile | string)[];
|
||||||
}) => {
|
}) => {
|
||||||
const id = pubkeyToString(pubkey);
|
const id = pubkeyToString(pubkey);
|
||||||
|
|
||||||
|
@ -261,3 +263,113 @@ export const ArtContent = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum ArtFetchStatus {
|
||||||
|
ReadyToFetch,
|
||||||
|
Fetching,
|
||||||
|
FetchFailed,
|
||||||
|
FetchSucceeded,
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedImages = new Map<string, string>();
|
||||||
|
export const useCachedImage = (uri: string) => {
|
||||||
|
const [cachedBlob, setCachedBlob] = useState<string | undefined>(undefined);
|
||||||
|
const [fetchStatus, setFetchStatus] = useState<ArtFetchStatus>(
|
||||||
|
ArtFetchStatus.ReadyToFetch
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!uri) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetchStatus === ArtFetchStatus.FetchFailed) {
|
||||||
|
setCachedBlob(uri);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = cachedImages.get(uri);
|
||||||
|
if (result) {
|
||||||
|
setCachedBlob(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetchStatus === ArtFetchStatus.ReadyToFetch) {
|
||||||
|
(async () => {
|
||||||
|
setFetchStatus(ArtFetchStatus.Fetching);
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(uri, { cache: "force-cache" });
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
response = await fetch(uri, { cache: "reload" });
|
||||||
|
} catch {
|
||||||
|
if (uri?.startsWith("http")) {
|
||||||
|
setCachedBlob(uri);
|
||||||
|
}
|
||||||
|
setFetchStatus(ArtFetchStatus.FetchFailed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobURI = URL.createObjectURL(blob);
|
||||||
|
cachedImages.set(uri, blobURI);
|
||||||
|
setCachedBlob(blobURI);
|
||||||
|
setFetchStatus(ArtFetchStatus.FetchSucceeded);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, [uri, setCachedBlob, fetchStatus, setFetchStatus]);
|
||||||
|
|
||||||
|
return { cachedBlob };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExtendedArt = (id: string, metadata: MetadataData) => {
|
||||||
|
const [data, setData] = useState<MetadataJson>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id && !data) {
|
||||||
|
if (metadata.data.uri) {
|
||||||
|
const uri = metadata.data.uri;
|
||||||
|
|
||||||
|
const processJson = (extended: any) => {
|
||||||
|
if (!extended || extended?.properties?.files?.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extended?.image) {
|
||||||
|
extended.image = extended.image.startsWith("http")
|
||||||
|
? extended.image
|
||||||
|
: `${metadata.data.uri}/${extended.image}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extended;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
fetch(uri)
|
||||||
|
.then(async (_) => {
|
||||||
|
try {
|
||||||
|
const data = await _.json();
|
||||||
|
try {
|
||||||
|
localStorage.setItem(uri, JSON.stringify(data));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
setData(processJson(data));
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [id, data, setData, metadata.data.uri]);
|
||||||
|
|
||||||
|
return { data };
|
||||||
|
};
|
|
@ -1,113 +0,0 @@
|
||||||
import { IMetadataExtension, Metadata } from "metaplex/classes";
|
|
||||||
import { StringPublicKey } from "metaplex/types";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
enum ArtFetchStatus {
|
|
||||||
ReadyToFetch,
|
|
||||||
Fetching,
|
|
||||||
FetchFailed,
|
|
||||||
FetchSucceeded,
|
|
||||||
}
|
|
||||||
|
|
||||||
const cachedImages = new Map<string, string>();
|
|
||||||
export const useCachedImage = (uri: string) => {
|
|
||||||
const [cachedBlob, setCachedBlob] = useState<string | undefined>(undefined);
|
|
||||||
const [fetchStatus, setFetchStatus] = useState<ArtFetchStatus>(
|
|
||||||
ArtFetchStatus.ReadyToFetch
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!uri) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fetchStatus === ArtFetchStatus.FetchFailed) {
|
|
||||||
setCachedBlob(uri);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = cachedImages.get(uri);
|
|
||||||
if (result) {
|
|
||||||
setCachedBlob(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fetchStatus === ArtFetchStatus.ReadyToFetch) {
|
|
||||||
(async () => {
|
|
||||||
setFetchStatus(ArtFetchStatus.Fetching);
|
|
||||||
let response: Response;
|
|
||||||
try {
|
|
||||||
response = await fetch(uri, { cache: "force-cache" });
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
response = await fetch(uri, { cache: "reload" });
|
|
||||||
} catch {
|
|
||||||
if (uri?.startsWith("http")) {
|
|
||||||
setCachedBlob(uri);
|
|
||||||
}
|
|
||||||
setFetchStatus(ArtFetchStatus.FetchFailed);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await response.blob();
|
|
||||||
const blobURI = URL.createObjectURL(blob);
|
|
||||||
cachedImages.set(uri, blobURI);
|
|
||||||
setCachedBlob(blobURI);
|
|
||||||
setFetchStatus(ArtFetchStatus.FetchSucceeded);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}, [uri, setCachedBlob, fetchStatus, setFetchStatus]);
|
|
||||||
|
|
||||||
return { cachedBlob };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useExtendedArt = (id: StringPublicKey, metadata: Metadata) => {
|
|
||||||
const [data, setData] = useState<IMetadataExtension>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (id && !data) {
|
|
||||||
if (metadata.data.uri) {
|
|
||||||
const uri = metadata.data.uri;
|
|
||||||
|
|
||||||
const processJson = (extended: any) => {
|
|
||||||
if (!extended || extended?.properties?.files?.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extended?.image) {
|
|
||||||
extended.image = extended.image.startsWith("http")
|
|
||||||
? extended.image
|
|
||||||
: `${metadata.data.uri}/${extended.image}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return extended;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
fetch(uri)
|
|
||||||
.then(async (_) => {
|
|
||||||
try {
|
|
||||||
const data = await _.json();
|
|
||||||
try {
|
|
||||||
localStorage.setItem(uri, JSON.stringify(data));
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
setData(processJson(data));
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
} catch (ex) {
|
|
||||||
console.error(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [id, data, setData, metadata.data.uri]);
|
|
||||||
|
|
||||||
return { data };
|
|
||||||
};
|
|
|
@ -1,296 +0,0 @@
|
||||||
/*
|
|
||||||
Taken from: https://github.com/metaplex-foundation/metaplex/blob/master/js/packages/common/src/actions/metadata.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import BN from "bn.js";
|
|
||||||
import {
|
|
||||||
StringPublicKey,
|
|
||||||
EDITION_MARKER_BIT_SIZE,
|
|
||||||
MetadataKey,
|
|
||||||
FileOrString,
|
|
||||||
MetadataCategory,
|
|
||||||
MetaplexKey,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
export class MasterEditionV1 {
|
|
||||||
key: MetadataKey;
|
|
||||||
supply: BN;
|
|
||||||
maxSupply?: BN;
|
|
||||||
/// Can be used to mint tokens that give one-time permission to mint a single limited edition.
|
|
||||||
printingMint: StringPublicKey;
|
|
||||||
/// If you don't know how many printing tokens you are going to need, but you do know
|
|
||||||
/// you are going to need some amount in the future, you can use a token from this mint.
|
|
||||||
/// Coming back to token metadata with one of these tokens allows you to mint (one time)
|
|
||||||
/// any number of printing tokens you want. This is used for instance by Auction Manager
|
|
||||||
/// with participation NFTs, where we dont know how many people will bid and need participation
|
|
||||||
/// printing tokens to redeem, so we give it ONE of these tokens to use after the auction is over,
|
|
||||||
/// because when the auction begins we just dont know how many printing tokens we will need,
|
|
||||||
/// but at the end we will. At the end it then burns this token with token-metadata to
|
|
||||||
/// get the printing tokens it needs to give to bidders. Each bidder then redeems a printing token
|
|
||||||
/// to get their limited editions.
|
|
||||||
oneTimePrintingAuthorizationMint: StringPublicKey;
|
|
||||||
|
|
||||||
constructor(args: {
|
|
||||||
key: MetadataKey;
|
|
||||||
supply: BN;
|
|
||||||
maxSupply?: BN;
|
|
||||||
printingMint: StringPublicKey;
|
|
||||||
oneTimePrintingAuthorizationMint: StringPublicKey;
|
|
||||||
}) {
|
|
||||||
this.key = MetadataKey.MasterEditionV1;
|
|
||||||
this.supply = args.supply;
|
|
||||||
this.maxSupply = args.maxSupply;
|
|
||||||
this.printingMint = args.printingMint;
|
|
||||||
this.oneTimePrintingAuthorizationMint =
|
|
||||||
args.oneTimePrintingAuthorizationMint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MasterEditionV2 {
|
|
||||||
key: MetadataKey;
|
|
||||||
supply: BN;
|
|
||||||
maxSupply?: BN;
|
|
||||||
|
|
||||||
constructor(args: { key: MetadataKey; supply: BN; maxSupply?: BN }) {
|
|
||||||
this.key = MetadataKey.MasterEditionV2;
|
|
||||||
this.supply = args.supply;
|
|
||||||
this.maxSupply = args.maxSupply;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EditionMarker {
|
|
||||||
key: MetadataKey;
|
|
||||||
ledger: number[];
|
|
||||||
|
|
||||||
constructor(args: { key: MetadataKey; ledger: number[] }) {
|
|
||||||
this.key = MetadataKey.EditionMarker;
|
|
||||||
this.ledger = args.ledger;
|
|
||||||
}
|
|
||||||
|
|
||||||
editionTaken(edition: number) {
|
|
||||||
const editionOffset = edition % EDITION_MARKER_BIT_SIZE;
|
|
||||||
const indexOffset = Math.floor(editionOffset / 8);
|
|
||||||
|
|
||||||
if (indexOffset > 30) {
|
|
||||||
throw Error("bad index for edition");
|
|
||||||
}
|
|
||||||
|
|
||||||
const positionInBitsetFromRight = 7 - (editionOffset % 8);
|
|
||||||
|
|
||||||
const mask = Math.pow(2, positionInBitsetFromRight);
|
|
||||||
|
|
||||||
const appliedMask = this.ledger[indexOffset] & mask;
|
|
||||||
|
|
||||||
return appliedMask !== 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Edition {
|
|
||||||
key: MetadataKey;
|
|
||||||
/// Points at MasterEdition struct
|
|
||||||
parent: StringPublicKey;
|
|
||||||
/// Starting at 0 for master record, this is incremented for each edition minted.
|
|
||||||
edition: BN;
|
|
||||||
|
|
||||||
constructor(args: {
|
|
||||||
key: MetadataKey;
|
|
||||||
parent: StringPublicKey;
|
|
||||||
edition: BN;
|
|
||||||
}) {
|
|
||||||
this.key = MetadataKey.EditionV1;
|
|
||||||
this.parent = args.parent;
|
|
||||||
this.edition = args.edition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export class Creator {
|
|
||||||
address: StringPublicKey;
|
|
||||||
verified: boolean;
|
|
||||||
share: number;
|
|
||||||
|
|
||||||
constructor(args: {
|
|
||||||
address: StringPublicKey;
|
|
||||||
verified: boolean;
|
|
||||||
share: number;
|
|
||||||
}) {
|
|
||||||
this.address = args.address;
|
|
||||||
this.verified = args.verified;
|
|
||||||
this.share = args.share;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Data {
|
|
||||||
name: string;
|
|
||||||
symbol: string;
|
|
||||||
uri: string;
|
|
||||||
sellerFeeBasisPoints: number;
|
|
||||||
creators: Creator[] | null;
|
|
||||||
constructor(args: {
|
|
||||||
name: string;
|
|
||||||
symbol: string;
|
|
||||||
uri: string;
|
|
||||||
sellerFeeBasisPoints: number;
|
|
||||||
creators: Creator[] | null;
|
|
||||||
}) {
|
|
||||||
this.name = args.name;
|
|
||||||
this.symbol = args.symbol;
|
|
||||||
this.uri = args.uri;
|
|
||||||
this.sellerFeeBasisPoints = args.sellerFeeBasisPoints;
|
|
||||||
this.creators = args.creators;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Metadata {
|
|
||||||
key: MetadataKey;
|
|
||||||
updateAuthority: StringPublicKey;
|
|
||||||
mint: StringPublicKey;
|
|
||||||
data: Data;
|
|
||||||
primarySaleHappened: boolean;
|
|
||||||
isMutable: boolean;
|
|
||||||
editionNonce: number | null;
|
|
||||||
|
|
||||||
constructor(args: {
|
|
||||||
updateAuthority: StringPublicKey;
|
|
||||||
mint: StringPublicKey;
|
|
||||||
data: Data;
|
|
||||||
primarySaleHappened: boolean;
|
|
||||||
isMutable: boolean;
|
|
||||||
editionNonce: number | null;
|
|
||||||
}) {
|
|
||||||
this.key = MetadataKey.MetadataV1;
|
|
||||||
this.updateAuthority = args.updateAuthority;
|
|
||||||
this.mint = args.mint;
|
|
||||||
this.data = args.data;
|
|
||||||
this.primarySaleHappened = args.primarySaleHappened;
|
|
||||||
this.isMutable = args.isMutable;
|
|
||||||
this.editionNonce = args.editionNonce;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMetadataExtension {
|
|
||||||
name: string;
|
|
||||||
symbol: string;
|
|
||||||
|
|
||||||
creators: Creator[] | null;
|
|
||||||
description: string;
|
|
||||||
// preview image absolute URI
|
|
||||||
image: string;
|
|
||||||
animation_url?: string;
|
|
||||||
|
|
||||||
// stores link to item on meta
|
|
||||||
external_url: string;
|
|
||||||
|
|
||||||
seller_fee_basis_points: number;
|
|
||||||
|
|
||||||
properties: {
|
|
||||||
files?: FileOrString[];
|
|
||||||
category: MetadataCategory;
|
|
||||||
maxSupply?: number;
|
|
||||||
creators?: {
|
|
||||||
address: string;
|
|
||||||
shares: number;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const METADATA_SCHEMA = new Map<any, any>([
|
|
||||||
[
|
|
||||||
MasterEditionV1,
|
|
||||||
{
|
|
||||||
kind: "struct",
|
|
||||||
fields: [
|
|
||||||
["key", "u8"],
|
|
||||||
["supply", "u64"],
|
|
||||||
["maxSupply", { kind: "option", type: "u64" }],
|
|
||||||
["printingMint", "pubkeyAsString"],
|
|
||||||
["oneTimePrintingAuthorizationMint", "pubkeyAsString"],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
MasterEditionV2,
|
|
||||||
{
|
|
||||||
kind: "struct",
|
|
||||||
fields: [
|
|
||||||
["key", "u8"],
|
|
||||||
["supply", "u64"],
|
|
||||||
["maxSupply", { kind: "option", type: "u64" }],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
Edition,
|
|
||||||
{
|
|
||||||
kind: "struct",
|
|
||||||
fields: [
|
|
||||||
["key", "u8"],
|
|
||||||
["parent", "pubkeyAsString"],
|
|
||||||
["edition", "u64"],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
Data,
|
|
||||||
{
|
|
||||||
kind: "struct",
|
|
||||||
fields: [
|
|
||||||
["name", "string"],
|
|
||||||
["symbol", "string"],
|
|
||||||
["uri", "string"],
|
|
||||||
["sellerFeeBasisPoints", "u16"],
|
|
||||||
["creators", { kind: "option", type: [Creator] }],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
Creator,
|
|
||||||
{
|
|
||||||
kind: "struct",
|
|
||||||
fields: [
|
|
||||||
["address", "pubkeyAsString"],
|
|
||||||
["verified", "u8"],
|
|
||||||
["share", "u8"],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
Metadata,
|
|
||||||
{
|
|
||||||
kind: "struct",
|
|
||||||
fields: [
|
|
||||||
["key", "u8"],
|
|
||||||
["updateAuthority", "pubkeyAsString"],
|
|
||||||
["mint", "pubkeyAsString"],
|
|
||||||
["data", Data],
|
|
||||||
["primarySaleHappened", "u8"], // bool
|
|
||||||
["isMutable", "u8"], // bool
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
EditionMarker,
|
|
||||||
{
|
|
||||||
kind: "struct",
|
|
||||||
fields: [
|
|
||||||
["key", "u8"],
|
|
||||||
["ledger", [31]],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
export class WhitelistedCreator {
|
|
||||||
key: MetaplexKey = MetaplexKey.WhitelistedCreatorV1;
|
|
||||||
address: StringPublicKey;
|
|
||||||
activated: boolean = true;
|
|
||||||
|
|
||||||
// Populated from name service
|
|
||||||
twitter?: string;
|
|
||||||
name?: string;
|
|
||||||
image?: string;
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
constructor(args: { address: string; activated: boolean }) {
|
|
||||||
this.address = args.address;
|
|
||||||
this.activated = args.activated;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
/*
|
|
||||||
Taken from: https://github.com/metaplex-foundation/metaplex/blob/master/js/packages/common/src/utils/ids.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { PublicKey, AccountInfo } from "@solana/web3.js";
|
|
||||||
|
|
||||||
export type StringPublicKey = string;
|
|
||||||
|
|
||||||
export class LazyAccountInfoProxy<T> {
|
|
||||||
executable: boolean = false;
|
|
||||||
owner: StringPublicKey = "";
|
|
||||||
lamports: number = 0;
|
|
||||||
|
|
||||||
get data() {
|
|
||||||
//
|
|
||||||
return undefined as unknown as T;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LazyAccountInfo {
|
|
||||||
executable: boolean;
|
|
||||||
owner: StringPublicKey;
|
|
||||||
lamports: number;
|
|
||||||
data: [string, string];
|
|
||||||
}
|
|
||||||
|
|
||||||
const PubKeysInternedMap = new Map<string, PublicKey>();
|
|
||||||
|
|
||||||
export const toPublicKey = (key: string | PublicKey) => {
|
|
||||||
if (typeof key !== "string") {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = PubKeysInternedMap.get(key);
|
|
||||||
if (!result) {
|
|
||||||
result = new PublicKey(key);
|
|
||||||
PubKeysInternedMap.set(key, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface PublicKeyStringAndAccount<T> {
|
|
||||||
pubkey: string;
|
|
||||||
account: AccountInfo<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = new PublicKey(
|
|
||||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
|
||||||
);
|
|
||||||
|
|
||||||
export const BPF_UPGRADE_LOADER_ID = new PublicKey(
|
|
||||||
"BPFLoaderUpgradeab1e11111111111111111111111"
|
|
||||||
);
|
|
||||||
|
|
||||||
export const MEMO_ID = new PublicKey(
|
|
||||||
"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
|
|
||||||
);
|
|
||||||
|
|
||||||
export const METADATA_PROGRAM_ID =
|
|
||||||
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" as StringPublicKey;
|
|
||||||
|
|
||||||
export const VAULT_ID =
|
|
||||||
"vau1zxA2LbssAUEF7Gpw91zMM1LvXrvpzJtmZ58rPsn" as StringPublicKey;
|
|
||||||
|
|
||||||
export const AUCTION_ID =
|
|
||||||
"auctxRXPeJoc4817jDhf4HbjnhEcr1cCXenosMhK5R8" as StringPublicKey;
|
|
||||||
|
|
||||||
export const METAPLEX_ID =
|
|
||||||
"p1exdMJcjVao65QdewkaZRUnU6VPSXhus9n2GzWfh98" as StringPublicKey;
|
|
||||||
|
|
||||||
export const SYSTEM = new PublicKey("11111111111111111111111111111111");
|
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
Taken from: https://github.com/metaplex-foundation/metaplex/blob/master/js/packages/common/src/actions/metadata.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type StringPublicKey = string;
|
|
||||||
|
|
||||||
export const EDITION = "edition";
|
|
||||||
export const METADATA_PREFIX = "metadata";
|
|
||||||
|
|
||||||
export const MAX_AUCTION_DATA_EXTENDED_SIZE = 8 + 9 + 2 + 200;
|
|
||||||
|
|
||||||
export const MAX_NAME_LENGTH = 32;
|
|
||||||
export const MAX_SYMBOL_LENGTH = 10;
|
|
||||||
export const MAX_URI_LENGTH = 200;
|
|
||||||
export const MAX_CREATOR_LIMIT = 5;
|
|
||||||
export const EDITION_MARKER_BIT_SIZE = 248;
|
|
||||||
export const MAX_CREATOR_LEN = 32 + 1 + 1;
|
|
||||||
export const MAX_METADATA_LEN =
|
|
||||||
1 +
|
|
||||||
32 +
|
|
||||||
32 +
|
|
||||||
MAX_NAME_LENGTH +
|
|
||||||
MAX_SYMBOL_LENGTH +
|
|
||||||
MAX_URI_LENGTH +
|
|
||||||
MAX_CREATOR_LIMIT * MAX_CREATOR_LEN +
|
|
||||||
2 +
|
|
||||||
1 +
|
|
||||||
1 +
|
|
||||||
198;
|
|
||||||
|
|
||||||
export enum MetadataKey {
|
|
||||||
Uninitialized = 0,
|
|
||||||
MetadataV1 = 4,
|
|
||||||
EditionV1 = 1,
|
|
||||||
MasterEditionV1 = 2,
|
|
||||||
MasterEditionV2 = 6,
|
|
||||||
EditionMarker = 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MetadataCategory {
|
|
||||||
Audio = "audio",
|
|
||||||
Video = "video",
|
|
||||||
Image = "image",
|
|
||||||
VR = "vr",
|
|
||||||
HTML = "html",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MetadataFile = {
|
|
||||||
uri: string;
|
|
||||||
type: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FileOrString = MetadataFile | string;
|
|
||||||
|
|
||||||
export interface Auction {
|
|
||||||
name: string;
|
|
||||||
auctionerName: string;
|
|
||||||
auctionerLink: string;
|
|
||||||
highestBid: number;
|
|
||||||
solAmt: number;
|
|
||||||
link: string;
|
|
||||||
image: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Artist {
|
|
||||||
address?: string;
|
|
||||||
name: string;
|
|
||||||
link: string;
|
|
||||||
image: string;
|
|
||||||
itemsAvailable?: number;
|
|
||||||
itemsSold?: number;
|
|
||||||
about?: string;
|
|
||||||
verified?: boolean;
|
|
||||||
|
|
||||||
share?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ArtType {
|
|
||||||
Master,
|
|
||||||
Print,
|
|
||||||
NFT,
|
|
||||||
}
|
|
||||||
export interface Art {
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MetaplexKey {
|
|
||||||
Uninitialized = 0,
|
|
||||||
OriginalAuthorityLookupV1 = 1,
|
|
||||||
BidRedemptionTicketV1 = 2,
|
|
||||||
StoreV1 = 3,
|
|
||||||
WhitelistedCreatorV1 = 4,
|
|
||||||
PayoutTicketV1 = 5,
|
|
||||||
SafetyDepositValidationTicketV1 = 6,
|
|
||||||
AuctionManagerV1 = 7,
|
|
||||||
PrizeTrackingTicketV1 = 8,
|
|
||||||
SafetyDepositConfigV1 = 9,
|
|
||||||
AuctionManagerV2 = 10,
|
|
||||||
BidRedemptionTicketV2 = 11,
|
|
||||||
AuctionWinnerTokenTypeTrackerV1 = 12,
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
/*
|
|
||||||
Taken from: https://github.com/metaplex-foundation/metaplex/blob/master/js/packages/common/src/utils/utils.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { PublicKey } from "@solana/web3.js";
|
|
||||||
|
|
||||||
export const pubkeyToString = (key: PublicKey | string = "") => {
|
|
||||||
return typeof key === "string" ? key : key?.toBase58() || "";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLast = <T>(arr: T[]) => {
|
|
||||||
if (arr.length <= 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return arr[arr.length - 1];
|
|
||||||
};
|
|
|
@ -25,12 +25,8 @@ import {
|
||||||
UpgradeableLoaderAccount,
|
UpgradeableLoaderAccount,
|
||||||
} from "validators/accounts/upgradeable-program";
|
} from "validators/accounts/upgradeable-program";
|
||||||
import { RewardsProvider } from "./rewards";
|
import { RewardsProvider } from "./rewards";
|
||||||
import { Metadata } from "metaplex/classes";
|
import { Metadata, MetadataData } from "@metaplex/js";
|
||||||
import {
|
import getEditionInfo, { EditionInfo } from "./utils/getEditionInfo";
|
||||||
EditionData,
|
|
||||||
getEditionData,
|
|
||||||
getMetadata,
|
|
||||||
} from "./utils/metadataHelpers";
|
|
||||||
export { useAccountHistory } from "./history";
|
export { useAccountHistory } from "./history";
|
||||||
|
|
||||||
export type StakeProgramData = {
|
export type StakeProgramData = {
|
||||||
|
@ -46,8 +42,8 @@ export type UpgradeableLoaderAccountData = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NFTData = {
|
export type NFTData = {
|
||||||
metadata: Metadata;
|
metadata: MetadataData;
|
||||||
editionData?: EditionData;
|
editionInfo: EditionInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TokenProgramData = {
|
export type TokenProgramData = {
|
||||||
|
@ -243,11 +239,17 @@ async function fetchAccountInfo(
|
||||||
|
|
||||||
// Generate a PDA and check for a Metadata Account
|
// Generate a PDA and check for a Metadata Account
|
||||||
if (parsed.type === "mint") {
|
if (parsed.type === "mint") {
|
||||||
const metadata = await getMetadata(pubkey, url);
|
const metadata = await Metadata.load(
|
||||||
|
connection,
|
||||||
|
await Metadata.getPDA(pubkey)
|
||||||
|
);
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
// We have a valid Metadata account. Try and pull edition data.
|
// We have a valid Metadata account. Try and pull edition data.
|
||||||
const editionData = await getEditionData(pubkey, url);
|
const editionInfo = await getEditionInfo(
|
||||||
nftData = { metadata, editionData };
|
metadata,
|
||||||
|
connection
|
||||||
|
);
|
||||||
|
nftData = { metadata: metadata.data, editionInfo };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import {
|
||||||
|
EditionData,
|
||||||
|
MasterEdition,
|
||||||
|
MasterEditionData,
|
||||||
|
Metadata,
|
||||||
|
MetadataKey,
|
||||||
|
} from "@metaplex/js";
|
||||||
|
import { Connection } from "@solana/web3.js";
|
||||||
|
|
||||||
|
export type EditionInfo = {
|
||||||
|
masterEdition?: MasterEditionData;
|
||||||
|
edition?: EditionData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function getEditionInfo(
|
||||||
|
metadata: Metadata,
|
||||||
|
connection: Connection
|
||||||
|
): Promise<EditionInfo> {
|
||||||
|
try {
|
||||||
|
const edition = (await metadata.getEdition(connection)).data;
|
||||||
|
|
||||||
|
if (edition) {
|
||||||
|
if (
|
||||||
|
edition.key === MetadataKey.MasterEditionV1 ||
|
||||||
|
edition.key === MetadataKey.MasterEditionV2
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
masterEdition: edition as MasterEditionData,
|
||||||
|
edition: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is an Edition NFT. Pull the Parent (MasterEdition)
|
||||||
|
const masterEdition = (
|
||||||
|
await MasterEdition.load(connection, (edition as EditionData).parent)
|
||||||
|
).data;
|
||||||
|
if (masterEdition) {
|
||||||
|
return {
|
||||||
|
masterEdition,
|
||||||
|
edition: edition as EditionData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
masterEdition: undefined,
|
||||||
|
edition: undefined,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,219 +0,0 @@
|
||||||
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js";
|
|
||||||
import {
|
|
||||||
Edition,
|
|
||||||
MasterEditionV1,
|
|
||||||
MasterEditionV2,
|
|
||||||
Metadata,
|
|
||||||
METADATA_SCHEMA,
|
|
||||||
} from "metaplex/classes";
|
|
||||||
import { MetadataKey, METADATA_PREFIX, StringPublicKey } from "metaplex/types";
|
|
||||||
import { deserializeUnchecked, BinaryReader, BinaryWriter } from "borsh";
|
|
||||||
import base58 from "bs58";
|
|
||||||
import {
|
|
||||||
METADATA_PROGRAM_ID,
|
|
||||||
SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
|
|
||||||
METAPLEX_ID,
|
|
||||||
BPF_UPGRADE_LOADER_ID,
|
|
||||||
SYSTEM,
|
|
||||||
MEMO_ID,
|
|
||||||
VAULT_ID,
|
|
||||||
AUCTION_ID,
|
|
||||||
toPublicKey,
|
|
||||||
} from "metaplex/ids";
|
|
||||||
import { TOKEN_PROGRAM_ID } from "providers/accounts/tokens";
|
|
||||||
|
|
||||||
let STORE: PublicKey | undefined;
|
|
||||||
|
|
||||||
export type EditionData = {
|
|
||||||
masterEdition?: MasterEditionV1 | MasterEditionV2;
|
|
||||||
edition?: Edition;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const programIds = () => {
|
|
||||||
return {
|
|
||||||
token: TOKEN_PROGRAM_ID,
|
|
||||||
associatedToken: SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
|
|
||||||
bpf_upgrade_loader: BPF_UPGRADE_LOADER_ID,
|
|
||||||
system: SYSTEM,
|
|
||||||
metadata: METADATA_PROGRAM_ID,
|
|
||||||
memo: MEMO_ID,
|
|
||||||
vault: VAULT_ID,
|
|
||||||
auction: AUCTION_ID,
|
|
||||||
metaplex: METAPLEX_ID,
|
|
||||||
store: STORE,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getMetadata(
|
|
||||||
pubkey: PublicKey,
|
|
||||||
url: string
|
|
||||||
): Promise<Metadata | undefined> {
|
|
||||||
const connection = new Connection(url, "confirmed");
|
|
||||||
const metadataKey = await generatePDA(pubkey);
|
|
||||||
const accountInfo = await connection.getAccountInfo(toPublicKey(metadataKey));
|
|
||||||
|
|
||||||
if (accountInfo && accountInfo.data.length > 0) {
|
|
||||||
if (!isMetadataAccount(accountInfo)) return;
|
|
||||||
|
|
||||||
if (isMetadataV1Account(accountInfo)) {
|
|
||||||
const metadata = decodeMetadata(accountInfo.data);
|
|
||||||
|
|
||||||
if (isValidHttpUrl(metadata.data.uri)) {
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEditionData(
|
|
||||||
pubkey: PublicKey,
|
|
||||||
url: string
|
|
||||||
): Promise<EditionData | undefined> {
|
|
||||||
const connection = new Connection(url, "confirmed");
|
|
||||||
const editionKey = await generatePDA(pubkey, true /* addEditionToSeeds */);
|
|
||||||
const accountInfo = await connection.getAccountInfo(toPublicKey(editionKey));
|
|
||||||
|
|
||||||
if (accountInfo && accountInfo.data.length > 0) {
|
|
||||||
if (!isMetadataAccount(accountInfo)) return;
|
|
||||||
|
|
||||||
if (isMasterEditionAccount(accountInfo)) {
|
|
||||||
return {
|
|
||||||
masterEdition: decodeMasterEdition(accountInfo.data),
|
|
||||||
edition: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is an Edition NFT. Pull the Parent (MasterEdition)
|
|
||||||
if (isEditionV1Account(accountInfo)) {
|
|
||||||
const edition = decodeEdition(accountInfo.data);
|
|
||||||
const masterEditionAccountInfo = await connection.getAccountInfo(
|
|
||||||
toPublicKey(edition.parent)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
masterEditionAccountInfo &&
|
|
||||||
masterEditionAccountInfo.data.length > 0 &&
|
|
||||||
isMasterEditionAccount(masterEditionAccountInfo)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
masterEdition: decodeMasterEdition(masterEditionAccountInfo.data),
|
|
||||||
edition,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generatePDA(
|
|
||||||
tokenMint: PublicKey,
|
|
||||||
addEditionToSeeds: boolean = false
|
|
||||||
): Promise<PublicKey> {
|
|
||||||
const PROGRAM_IDS = programIds();
|
|
||||||
|
|
||||||
const metadataSeeds = [
|
|
||||||
Buffer.from(METADATA_PREFIX),
|
|
||||||
toPublicKey(PROGRAM_IDS.metadata).toBuffer(),
|
|
||||||
tokenMint.toBuffer(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (addEditionToSeeds) {
|
|
||||||
metadataSeeds.push(Buffer.from("edition"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
await PublicKey.findProgramAddress(
|
|
||||||
metadataSeeds,
|
|
||||||
toPublicKey(PROGRAM_IDS.metadata)
|
|
||||||
)
|
|
||||||
)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodeMetadata = (buffer: Buffer): Metadata => {
|
|
||||||
const metadata = deserializeUnchecked(
|
|
||||||
METADATA_SCHEMA,
|
|
||||||
Metadata,
|
|
||||||
buffer
|
|
||||||
) as Metadata;
|
|
||||||
|
|
||||||
// Remove any trailing null characters from the deserialized strings
|
|
||||||
metadata.data.name = metadata.data.name.replace(/\0/g, "");
|
|
||||||
metadata.data.symbol = metadata.data.symbol.replace(/\0/g, "");
|
|
||||||
metadata.data.uri = metadata.data.uri.replace(/\0/g, "");
|
|
||||||
metadata.data.name = metadata.data.name.replace(/\0/g, "");
|
|
||||||
return metadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const decodeMasterEdition = (
|
|
||||||
buffer: Buffer
|
|
||||||
): MasterEditionV1 | MasterEditionV2 => {
|
|
||||||
if (buffer[0] === MetadataKey.MasterEditionV1) {
|
|
||||||
return deserializeUnchecked(
|
|
||||||
METADATA_SCHEMA,
|
|
||||||
MasterEditionV1,
|
|
||||||
buffer
|
|
||||||
) as MasterEditionV1;
|
|
||||||
} else {
|
|
||||||
return deserializeUnchecked(
|
|
||||||
METADATA_SCHEMA,
|
|
||||||
MasterEditionV2,
|
|
||||||
buffer
|
|
||||||
) as MasterEditionV2;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const decodeEdition = (buffer: Buffer) => {
|
|
||||||
return deserializeUnchecked(METADATA_SCHEMA, Edition, buffer) as Edition;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isMetadataAccount = (account: AccountInfo<Buffer>) =>
|
|
||||||
account.owner.toBase58() === METADATA_PROGRAM_ID;
|
|
||||||
|
|
||||||
const isMetadataV1Account = (account: AccountInfo<Buffer>) =>
|
|
||||||
account.data[0] === MetadataKey.MetadataV1;
|
|
||||||
|
|
||||||
const isEditionV1Account = (account: AccountInfo<Buffer>) =>
|
|
||||||
account.data[0] === MetadataKey.EditionV1;
|
|
||||||
|
|
||||||
const isMasterEditionAccount = (account: AccountInfo<Buffer>) =>
|
|
||||||
account.data[0] === MetadataKey.MasterEditionV1 ||
|
|
||||||
account.data[0] === MetadataKey.MasterEditionV2;
|
|
||||||
|
|
||||||
function isValidHttpUrl(text: string) {
|
|
||||||
try {
|
|
||||||
const url = new URL(text);
|
|
||||||
return url.protocol === "http:" || url.protocol === "https:";
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Required to properly serialize and deserialize pubKeyAsString types
|
|
||||||
const extendBorsh = () => {
|
|
||||||
(BinaryReader.prototype as any).readPubkey = function () {
|
|
||||||
const reader = this as unknown as BinaryReader;
|
|
||||||
const array = reader.readFixedArray(32);
|
|
||||||
return new PublicKey(array);
|
|
||||||
};
|
|
||||||
|
|
||||||
(BinaryWriter.prototype as any).writePubkey = function (value: any) {
|
|
||||||
const writer = this as unknown as BinaryWriter;
|
|
||||||
writer.writeFixedArray(value.toBuffer());
|
|
||||||
};
|
|
||||||
|
|
||||||
(BinaryReader.prototype as any).readPubkeyAsString = function () {
|
|
||||||
const reader = this as unknown as BinaryReader;
|
|
||||||
const array = reader.readFixedArray(32);
|
|
||||||
return base58.encode(array) as StringPublicKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
(BinaryWriter.prototype as any).writePubkeyAsString = function (
|
|
||||||
value: StringPublicKey
|
|
||||||
) {
|
|
||||||
const writer = this as unknown as BinaryWriter;
|
|
||||||
writer.writeFixedArray(base58.decode(value));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
extendBorsh();
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React from "react";
|
|
||||||
import BN from "bn.js";
|
import BN from "bn.js";
|
||||||
import {
|
import {
|
||||||
HumanizeDuration,
|
HumanizeDuration,
|
||||||
HumanizeDurationLanguage,
|
HumanizeDurationLanguage,
|
||||||
} from "humanize-duration-ts";
|
} from "humanize-duration-ts";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
|
||||||
// Switch to web3 constant when web3 updates superstruct
|
// Switch to web3 constant when web3 updates superstruct
|
||||||
export const LAMPORTS_PER_SOL = 1000000000;
|
export const LAMPORTS_PER_SOL = 1000000000;
|
||||||
|
@ -133,3 +133,15 @@ export function abbreviatedNumber(value: number, fixed = 1) {
|
||||||
if (value >= 1e9 && value < 1e12) return +(value / 1e9).toFixed(fixed) + "B";
|
if (value >= 1e9 && value < 1e12) return +(value / 1e9).toFixed(fixed) + "B";
|
||||||
if (value >= 1e12) return +(value / 1e12).toFixed(fixed) + "T";
|
if (value >= 1e12) return +(value / 1e12).toFixed(fixed) + "T";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const pubkeyToString = (key: PublicKey | string = "") => {
|
||||||
|
return typeof key === "string" ? key : key?.toBase58() || "";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLast = (arr: string[]) => {
|
||||||
|
if (arr.length <= 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr[arr.length - 1];
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue