Skip to content

Commit 81ef5da

Browse files
committed
Moved to umi, need to test nft functionality
1 parent f235f5b commit 81ef5da

File tree

8 files changed

+546
-3263
lines changed

8 files changed

+546
-3263
lines changed

.yarn/install-state.gz

-57.1 KB
Binary file not shown.

package-lock.json

Lines changed: 236 additions & 1596 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"@coral-xyz/anchor": "^0.30.1",
1919
"@jup-ag/api": "^6.0.24",
2020
"@jup-ag/referral-sdk": "0.1.7",
21-
"@metaplex-foundation/js": "^0.20.1",
21+
"@metaplex-foundation/mpl-token-metadata": "3.3.0",
22+
"@metaplex-foundation/umi": "^0.8.9",
23+
"@metaplex-foundation/umi-bundle-defaults": "^0.8.9",
2224
"@solana/spl-name-service": "^0.1.4",
2325
"@solana/spl-token": "^0.3.7",
2426
"@solana/spl-token-registry": "^0.2.4574",

src/utils/endpoints.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const JUPITERSWAP = "https://quote-api.jup.ag/v6/swap";
1616
export const JUPITERSWAPINSTRUCTIONS = "https://quote-api.jup.ag/v6/swap-instructions";
1717
export const SIMPLEHASHSOLANA = "https://api.simplehash.com/api/v0/nfts/solana"
1818
export const BLOCKENGINE_URL = `amsterdam.mainnet.block-engine.jito.wtf`
19+
export const DEXSCREENER = "https://api.dexscreener.com";
1920

2021
export const RPC_URL = process.env.NEXT_PUBLIC_RPC_URL;
2122
// You can use any of the other enpoints here

src/utils/extractCidFromUrl.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
export const extractCidFromUrl = (url: string): string | null => {
2-
if (!url) {
3-
console.error("No IPFS URL provided");
4-
return "";
5-
}
6-
7-
let cid = "";
8-
if (url.startsWith("https://cf-ipfs.com/ipfs/")) {
9-
cid = url.replace("https://cf-ipfs.com/ipfs/", "");
10-
} else if (url.startsWith("https://ipfs.io/ipfs/")) {
11-
cid = url.replace("https://ipfs.io/ipfs/", "");
12-
} else if (url.startsWith("ipfs://")) {
13-
cid = url.replace("ipfs://", "");
14-
} else {
15-
const urlParts = url.split("/");
16-
cid = urlParts.find((part) => part.length === 46 && part.startsWith("Qm")) ?? "";
17-
}
18-
19-
return cid.toString();
20-
};
21-
2+
if (!url) {
3+
console.error("No IPFS URL provided");
4+
return "";
5+
}
6+
7+
let cid = "";
8+
if (url.startsWith("https://cf-ipfs.com/ipfs/")) {
9+
cid = url.replace("https://cf-ipfs.com/ipfs/", "");
10+
} else if (url.startsWith("https://ipfs.io/ipfs/")) {
11+
cid = url.replace("https://ipfs.io/ipfs/", "");
12+
} else if (url.startsWith("https://nftstorage.link/ipfs/")) {
13+
cid = url.replace("https://nftstorage.link/ipfs/", "");
14+
} else if (url.startsWith("ipfs://")) {
15+
cid = url.replace("ipfs://", "");
16+
} else {
17+
const urlParts = url.split("/");
18+
// Match either CIDv0 (Qm..., 46 chars) or CIDv1 (baf..., 59 chars)
19+
cid = urlParts.find((part) =>
20+
(part.length === 46 && part.startsWith("Qm")) ||
21+
(part.length === 59 && part.startsWith("baf"))
22+
) ?? "";
23+
}
24+
25+
return cid.toString();
26+
};

src/utils/fetchDexTokenInfo.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { DEXSCREENER } from "./endpoints";
2+
3+
// Helper function to fetch token info from contract address
4+
export async function getTokenInfo(address: string) {
5+
try {
6+
const response = await fetch(`${DEXSCREENER}/latest/dex/tokens/${address}`);
7+
if (!response.ok) return null;
8+
9+
const data = await response.json();
10+
const pair = data.pairs?.[0]; // Get first pair's information
11+
12+
13+
if (!pair?.baseToken) return null;
14+
15+
// Debug log
16+
// console.log('DexScreener response for', address, ':', {
17+
// priceUsd: pair.priceUsd,
18+
// priceNative: pair.priceNative,
19+
// baseToken: pair.baseToken
20+
// });
21+
22+
return {
23+
name: pair.baseToken.name,
24+
contractAddress: address,
25+
symbol: pair.baseToken.symbol,
26+
decimals: pair.baseToken.decimals,
27+
marketCap: pair.marketCap,
28+
price: pair.priceUsd || 0,
29+
priceNative: pair.priceNative || 0,
30+
image: pair.info?.imageUrl,
31+
website: pair.info?.websites,
32+
};
33+
} catch (error) {
34+
console.error("Error fetching token info for", address, ":", error);
35+
return null;
36+
}
37+
}

src/utils/tokenUtils.ts

Lines changed: 68 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import { AddressLookupTableAccount, Connection, PublicKey, PublicKeyInitData, TransactionInstruction } from "@solana/web3.js";
2-
import { Metaplex } from "@metaplex-foundation/js";
2+
// import { Metaplex } from "@metaplex-foundation/js";
3+
import {
4+
fetchDigitalAssetWithAssociatedToken,
5+
mplTokenMetadata,
6+
findMetadataPda,
7+
fetchDigitalAssetByMetadata,
8+
fetchMetadata,
9+
TokenStandard
10+
} from '@metaplex-foundation/mpl-token-metadata'
11+
import { fromWeb3JsPublicKey, toWeb3JsPublicKey } from '@metaplex-foundation/umi-web3js-adapters'
12+
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
313
import Bottleneck from "bottleneck";
414
import { fetchIpfsMetadata } from "./fetchIpfsMetadata";
515
import { extractCidFromUrl } from "./extractCidFromUrl";
@@ -10,6 +20,7 @@ import { Instruction } from "@jup-ag/api";
1020
import { fetchFloorPrice } from "./fetchFloorPrice";
1121
import { NETWORK } from "@utils/endpoints";
1222
import { createJupiterApiClient, QuoteGetRequest } from "@jup-ag/api";
23+
import { getTokenInfo } from "./fetchDexTokenInfo";
1324

1425
const ENDPOINT = NETWORK;
1526

@@ -21,7 +32,7 @@ if (!ENDPOINT || (!ENDPOINT.startsWith('http:') && !ENDPOINT.startsWith('https:'
2132

2233
// console.log(`ENDPOINT: ${ENDPOINT}`);
2334
const connection = new Connection(ENDPOINT);
24-
const metaplex = Metaplex.make(connection);
35+
const metaplexUmi = createUmi(ENDPOINT).use(mplTokenMetadata());
2536
const DEFAULT_IMAGE_URL = process.env.UNKNOWN_IMAGE_URL || "https://s3.coinmarketcap.com/static-gravity/image/5cc0b99a8dd84fbfa4e150d84b5531f2.png";
2637

2738
// Modify the rate limiters at the top
@@ -113,34 +124,47 @@ export type TokenData = {
113124

114125
export async function fetchTokenMetadata(mintAddress: PublicKey, mint: string) {
115126
try {
116-
const metadataAccount = metaplex
117-
.nfts()
118-
.pdas()
119-
.metadata({ mint: mintAddress });
120-
121-
const metadataAccountInfo = await withRetry(() =>
122-
rpcLimiter.schedule(() => connection.getAccountInfo(metadataAccount))
123-
);
127+
const metadataPda = findMetadataPda(metaplexUmi, {
128+
mint: fromWeb3JsPublicKey(mintAddress)
129+
});
130+
// console.log(`Metadata account: ${metadataPda}`);
131+
// const metadataAccountInfo = await withRetry(() =>
132+
// rpcLimiter.schedule(() => connection.getAccountInfo(new PublicKey(metadataPda)))
133+
// );
134+
const metadataAccountInfo = await fetchDigitalAssetByMetadata(metaplexUmi, metadataPda);
135+
136+
console.log(`Metadata: ${JSON.stringify(metadataAccountInfo, (_, value) =>
137+
typeof value === 'bigint' ? value.toString() : value
138+
)}`);
124139

125140
if (!metadataAccountInfo) {
126141
return getDefaultTokenMetadata(mint);
127142
}
143+
const collectionMetadata = await fetchMetadata(metaplexUmi, metadataPda);
144+
const cid = collectionMetadata.uri ? extractCidFromUrl(collectionMetadata.uri) : null;
145+
const logo = cid ? await fetchIpfsMetadata(cid) : null;
128146

129147
const token = await withRetry(() =>
130-
rpcLimiter.schedule(() => metaplex.nfts().findByMint({ mintAddress: mintAddress }))
148+
rpcLimiter.schedule(() => fetchDigitalAssetByMetadata(metaplexUmi, metadataPda))
131149
);
132150

133-
let metadata = await processTokenMetadata(token, mint);
134-
151+
let metadata = await processTokenMetadata(token, logo?.imageUrl ?? '', cid ?? '', mint);
135152
// Handle collection metadata separately to prevent failures
136-
if (token.collection) {
153+
const tokenStandard = metadataAccountInfo?.metadata?.tokenStandard?.valueOf();
154+
const isNft = tokenStandard === TokenStandard.NonFungible ||
155+
tokenStandard === TokenStandard.NonFungibleEdition ||
156+
tokenStandard === TokenStandard.ProgrammableNonFungible ||
157+
tokenStandard === TokenStandard.ProgrammableNonFungibleEdition;
158+
console.log(`isNft: ${isNft}`);
159+
if (isNft) {
160+
const collectionName = collectionMetadata?.name ?? metadata.name;
161+
const collectionLogo = logo?.imageUrl ?? DEFAULT_IMAGE_URL;
137162
try {
138-
const collectionMetadata = await fetchCollectionMetadata(token.collection.address);
139163
metadata = {
140164
...metadata,
141-
collectionName: collectionMetadata?.name ?? metadata.name,
142-
collectionLogo: collectionMetadata?.logo ?? metadata.logo,
143-
isNft: true
165+
collectionName,
166+
collectionLogo,
167+
isNft
144168
};
145169
} catch (collectionError) {
146170
console.warn(`Failed to fetch collection metadata for token ${mint}:`, collectionError);
@@ -156,119 +180,44 @@ export async function fetchTokenMetadata(mintAddress: PublicKey, mint: string) {
156180
}
157181
}
158182

159-
async function fetchCollectionMetadata(collectionAddress: PublicKey) {
160-
try {
161-
const metadataAccount = metaplex
162-
.nfts()
163-
.pdas()
164-
.metadata({ mint: collectionAddress });
165-
166-
// Wrap RPC calls with withRetry and rpcLimiter
167-
const metadataAccountInfo = await withRetry(() =>
168-
rpcLimiter.schedule(() =>
169-
connection.getAccountInfo(metadataAccount)
170-
)
171-
);
172-
173-
if (!metadataAccountInfo) {
174-
console.log(`No metadata account found for collection: ${collectionAddress.toString()}`);
175-
return getDefaultMetadata();
176-
}
177-
178-
const collection = await withRetry(() =>
179-
rpcLimiter.schedule(() =>
180-
metaplex.nfts().findByMint({ mintAddress: collectionAddress })
181-
)
182-
);
183-
184-
const cid = extractCidFromUrl(collection.uri);
185-
if (cid) {
186-
try {
187-
const collectionMetadata = await apiLimiter.schedule(() =>
188-
fetchIpfsMetadata(cid)
189-
);
190-
return {
191-
name: collection.name || "Unknown Collection",
192-
symbol: collection.symbol || "UNKNOWN",
193-
logo: collectionMetadata.imageUrl ?? collection.json?.image ?? DEFAULT_IMAGE_URL,
194-
cid: cid,
195-
isNft: true
196-
};
197-
} catch (ipfsError) {
198-
console.warn(`Failed to fetch IPFS metadata for collection ${collectionAddress.toString()}:`, ipfsError);
199-
return {
200-
name: collection.name || "Unknown Collection",
201-
symbol: collection.symbol || "UNKNOWN",
202-
logo: collection.json?.image ?? DEFAULT_IMAGE_URL,
203-
cid: cid,
204-
isNft: true
205-
};
206-
}
207-
}
208-
209-
return {
210-
name: collection.name || "Unknown Collection",
211-
symbol: collection.symbol || "UNKNOWN",
212-
logo: collection.json?.image ?? DEFAULT_IMAGE_URL,
213-
cid: null,
214-
isNft: true
215-
};
216-
217-
} catch (error) {
218-
console.warn("Error fetching collection metadata for address:", collectionAddress.toString(), error);
219-
return getDefaultMetadata();
220-
}
221-
}
222-
223-
// Add a helper function to return default metadata
224-
function getDefaultMetadata() {
225-
return {
226-
name: "Unknown Collection",
227-
symbol: "UNKNOWN",
228-
logo: DEFAULT_IMAGE_URL,
229-
cid: null,
230-
isNft: true
231-
};
232-
}
233-
234183
// Helper function to get default token metadata
235-
function getDefaultTokenMetadata(mint: string) {
184+
async function getDefaultTokenMetadata(mint: string) {
185+
const tokenInfo = await getTokenInfo(mint);
236186
return {
237-
name: mint,
238-
symbol: mint,
239-
logo: DEFAULT_IMAGE_URL,
187+
name: tokenInfo?.name || mint,
188+
symbol: tokenInfo?.symbol || mint,
189+
logo: tokenInfo?.image || DEFAULT_IMAGE_URL,
240190
cid: null,
241191
collectionName: mint,
242-
collectionLogo: DEFAULT_IMAGE_URL,
192+
collectionLogo: tokenInfo?.image || DEFAULT_IMAGE_URL,
243193
isNft: false
244194
};
245195
}
246196

247197
// Helper function to process token metadata
248-
async function processTokenMetadata(token: any, mint: string) {
249-
const cid = extractCidFromUrl(token.uri);
198+
async function processTokenMetadata(token: any, logo: string, cid: string, mint: string) {
199+
console.log(`Token metadata: ${JSON.stringify(token, (_, value) =>
200+
typeof value === 'bigint' ? value.toString() : value
201+
)}`);
202+
let tokenName = mint;
203+
let symbol = mint;
204+
if(logo.length > 0) {
205+
logo = logo;
206+
} else {
207+
const tokenInfo = await getTokenInfo(mint);
208+
tokenName = tokenInfo?.name ?? mint;
209+
symbol = tokenInfo?.symbol ?? mint;
210+
logo = tokenInfo?.image ?? DEFAULT_IMAGE_URL;
211+
}
250212
let metadata = {
251-
name: token?.name || mint,
252-
symbol: token?.symbol || mint,
253-
logo: token.json?.image ?? DEFAULT_IMAGE_URL,
254-
cid,
255-
collectionName: token?.name || mint,
256-
collectionLogo: token.json?.image ?? DEFAULT_IMAGE_URL,
213+
name: token?.metadata?.name || tokenName,
214+
symbol: token?.metadata?.symbol || symbol,
215+
logo: logo,
216+
cid: cid,
217+
collectionName: token?.metadata?.name || tokenName,
218+
collectionLogo: logo ?? DEFAULT_IMAGE_URL,
257219
isNft: false
258220
};
259-
260-
if (cid) {
261-
try {
262-
const newMetadata = await apiLimiter.schedule(() =>
263-
fetchIpfsMetadata(cid)
264-
);
265-
metadata.logo = newMetadata.imageUrl ?? token.json?.image ?? DEFAULT_IMAGE_URL;
266-
} catch (ipfsError) {
267-
console.warn(`Failed to fetch IPFS metadata for token ${mint}:`, ipfsError);
268-
// Keep existing metadata if IPFS fetch fails
269-
}
270-
}
271-
272221
return metadata;
273222
}
274223

0 commit comments

Comments
 (0)