Skip to content

Commit c0778cb

Browse files
committed
[SDK] Feature: Export uri parsing utils (#4565)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR adds `parseAvatarRecord` and `parseNftUri` utilities for ENS avatars and NFT URIs. Detailed summary: ### Detailed summary - Added `parseAvatarRecord` and `parseNftUri` functions for ENS avatars and NFT URIs - Updated `common.ts` and `ens.ts` with new exports - Created `parseNftUri` function in `parseNft.ts` - Added `parseAvatarRecord` function in `avatar.ts` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent db1f796 commit c0778cb

File tree

5 files changed

+142
-71
lines changed

5 files changed

+142
-71
lines changed

.changeset/mean-owls-nail.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Adds parseAvatarRecord and parseNftUri utilities
6+
7+
```ts
8+
import { parseAvatarRecord } from "thirdweb/extensions/ens";
9+
import { parseNftUri } from "thirdweb/extensions/common";
10+
11+
const avatarUrl = await parseAvatarRecord({
12+
client,
13+
uri: "...",
14+
});
15+
16+
const nftUri = await parseNftUri({
17+
client,
18+
uri: "...",
19+
});
20+
```

packages/thirdweb/src/exports/extensions/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export {
1313
symbol,
1414
isSymbolSupported,
1515
} from "../../extensions/common/read/symbol.js";
16+
export { parseNftUri } from "../../utils/nft/parseNft.js";
1617

1718
// write
1819
export {

packages/thirdweb/src/exports/extensions/ens.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ export {
88
resolveAvatar,
99
} from "../../extensions/ens/resolve-avatar.js";
1010

11+
export {
12+
parseAvatarRecord,
13+
type ParseAvatarOptions,
14+
} from "../../utils/ens/avatar.js";
15+
1116
export {
1217
type ResolveTextOptions,
1318
resolveText,

packages/thirdweb/src/utils/ens/avatar.ts

Lines changed: 27 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,40 @@
1-
import { getCachedChain } from "../../chains/utils.js";
21
import type { ThirdwebClient } from "../../client/client.js";
3-
import { getContract } from "../../contract/contract.js";
4-
import { isAddress } from "../address.js";
52
import { getClientFetch } from "../fetch.js";
63
import { resolveScheme } from "../ipfs.js";
4+
import { parseNftUri } from "../nft/parseNft.js";
75

8-
type AvatarOptions = {
6+
export type ParseAvatarOptions = {
97
client: ThirdwebClient;
108
uri: string;
119
};
1210

1311
/**
14-
* @internal
12+
* Parses an ENS or similar avatar record. Supports NFT URIs, IPFS scheme, and HTTPS URIs.
13+
* @param options - The options for parsing an ENS avatar record.
14+
* @param options.client - The Thirdweb client.
15+
* @param options.uri - The URI to parse.
16+
* @returns A promise that resolves to the avatar URL, or null if the URI could not be parsed.
17+
* @example
18+
* ```ts
19+
* import { parseAvatarRecord } from "thirdweb/utils/ens";
20+
* const avatarUrl = await parseAvatarRecord({
21+
* client,
22+
* uri: "ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/",
23+
* });
24+
*
25+
* console.log(avatarUrl); // "https://ipfs.io/ipfs/bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/"
26+
*
27+
* const avatarUrl2 = await parseAvatarRecord({
28+
* client,
29+
* uri: "eip155:1/erc1155:0xb32979486938aa9694bfc898f35dbed459f44424/10063",
30+
* });
31+
*
32+
* console.log(avatarUrl2); // "https://opensea.io/assets/0xb32979486938aa9694bfc898f35dbed459f44424/10063"
33+
* ```
34+
* @extension ENS
1535
*/
1636
export async function parseAvatarRecord(
17-
options: AvatarOptions,
37+
options: ParseAvatarOptions,
1838
): Promise<string | null> {
1939
let uri: string | null = options.uri;
2040
if (/eip155:/i.test(options.uri)) {
@@ -36,71 +56,7 @@ export async function parseAvatarRecord(
3656
return null;
3757
}
3858

39-
/**
40-
* @internal
41-
*/
42-
async function parseNftUri(options: AvatarOptions): Promise<string | null> {
43-
let uri = options.uri;
44-
// parse valid nft spec (CAIP-22/CAIP-29)
45-
// @see: https://github.com/ChainAgnostic/CAIPs/tree/master/CAIPs
46-
if (uri.startsWith("did:nft:")) {
47-
// convert DID to CAIP
48-
uri = uri.replace("did:nft:", "").replace(/_/g, "/");
49-
}
50-
51-
const [reference = "", asset_namespace = "", tokenID = ""] = uri.split("/");
52-
const [eip_namespace, chainID] = reference.split(":");
53-
const [erc_namespace, contractAddress] = asset_namespace.split(":");
54-
55-
if (!eip_namespace || eip_namespace.toLowerCase() !== "eip155") {
56-
throw new Error(
57-
`Invalid EIP namespace, expected EIP155, got: "${eip_namespace}"`,
58-
);
59-
}
60-
if (!chainID) {
61-
throw new Error("Chain ID not found");
62-
}
63-
if (!contractAddress || !isAddress(contractAddress)) {
64-
throw new Error("Contract address not found");
65-
}
66-
if (!tokenID) {
67-
throw new Error("Token ID not found");
68-
}
69-
const chain = getCachedChain(Number(chainID));
70-
const contract = getContract({
71-
client: options.client,
72-
chain,
73-
address: contractAddress,
74-
});
75-
switch (erc_namespace) {
76-
case "erc721": {
77-
const { getNFT } = await import("../../extensions/erc721/read/getNFT.js");
78-
const nft = await getNFT({
79-
contract,
80-
tokenId: BigInt(tokenID),
81-
});
82-
return nft.metadata.image ?? null;
83-
}
84-
case "erc1155": {
85-
const { getNFT } = await import(
86-
"../../extensions/erc1155/read/getNFT.js"
87-
);
88-
const nft = await getNFT({
89-
contract,
90-
tokenId: BigInt(tokenID),
91-
});
92-
return nft.metadata.image ?? null;
93-
}
94-
95-
default: {
96-
throw new Error(
97-
`Invalid ERC namespace, expected ERC721 or ERC1155, got: "${erc_namespace}"`,
98-
);
99-
}
100-
}
101-
}
102-
103-
async function isImageUri(options: AvatarOptions): Promise<boolean> {
59+
async function isImageUri(options: ParseAvatarOptions): Promise<boolean> {
10460
try {
10561
const res = await getClientFetch(options.client)(options.uri, {
10662
method: "HEAD",

packages/thirdweb/src/utils/nft/parseNft.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import { getCachedChain } from "../../chains/utils.js";
2+
import type { ThirdwebClient } from "../../client/client.js";
3+
import { getContract } from "../../contract/contract.js";
14
import type { FileOrBufferOrString } from "../../storage/upload/types.js";
5+
import { isAddress } from "../address.js";
26
import type { Prettify } from "../type-utils.js";
37

48
/**
@@ -95,3 +99,88 @@ export function parseNFT(base: NFTMetadata, options: ParseNFTOptions): NFT {
9599
throw new Error("Invalid NFT type");
96100
}
97101
}
102+
103+
/**
104+
* Parses an NFT URI.
105+
* @param options - The options for parsing an NFT URI.
106+
* @param options.client - The Thirdweb client.
107+
* @param options.uri - The NFT URI to parse.
108+
* @returns A promise that resolves to the NFT URI, or null if the URI could not be parsed.
109+
*
110+
* @example
111+
* ```ts
112+
* import { parseNftUri } from "thirdweb/utils/ens";
113+
* const nftUri = await parseNftUri({
114+
* client,
115+
* uri: "eip155:1/erc1155:0xb32979486938aa9694bfc898f35dbed459f44424/10063",
116+
* });
117+
*
118+
* console.log(nftUri); // ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/
119+
* ```
120+
*
121+
* @extension ENS
122+
*
123+
*/
124+
export async function parseNftUri(options: {
125+
client: ThirdwebClient;
126+
uri: string;
127+
}): Promise<string | null> {
128+
let uri = options.uri;
129+
// parse valid nft spec (CAIP-22/CAIP-29)
130+
// @see: https://github.com/ChainAgnostic/CAIPs/tree/master/CAIPs
131+
if (uri.startsWith("did:nft:")) {
132+
// convert DID to CAIP
133+
uri = uri.replace("did:nft:", "").replace(/_/g, "/");
134+
}
135+
136+
const [reference = "", asset_namespace = "", tokenID = ""] = uri.split("/");
137+
const [eip_namespace, chainID] = reference.split(":");
138+
const [erc_namespace, contractAddress] = asset_namespace.split(":");
139+
140+
if (!eip_namespace || eip_namespace.toLowerCase() !== "eip155") {
141+
throw new Error(
142+
`Invalid EIP namespace, expected EIP155, got: "${eip_namespace}"`,
143+
);
144+
}
145+
if (!chainID) {
146+
throw new Error("Chain ID not found");
147+
}
148+
if (!contractAddress || !isAddress(contractAddress)) {
149+
throw new Error("Contract address not found");
150+
}
151+
if (!tokenID) {
152+
throw new Error("Token ID not found");
153+
}
154+
const chain = getCachedChain(Number(chainID));
155+
const contract = getContract({
156+
client: options.client,
157+
chain,
158+
address: contractAddress,
159+
});
160+
switch (erc_namespace) {
161+
case "erc721": {
162+
const { getNFT } = await import("../../extensions/erc721/read/getNFT.js");
163+
const nft = await getNFT({
164+
contract,
165+
tokenId: BigInt(tokenID),
166+
});
167+
return nft.metadata.image ?? null;
168+
}
169+
case "erc1155": {
170+
const { getNFT } = await import(
171+
"../../extensions/erc1155/read/getNFT.js"
172+
);
173+
const nft = await getNFT({
174+
contract,
175+
tokenId: BigInt(tokenID),
176+
});
177+
return nft.metadata.image ?? null;
178+
}
179+
180+
default: {
181+
throw new Error(
182+
`Invalid ERC namespace, expected ERC721 or ERC1155, got: "${erc_namespace}"`,
183+
);
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)