Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .changeset/witty-plums-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
"thirdweb": minor
---

### `getContractMetadata()` now returns a record with `unknown` values instead of `any`.

before:
```ts
const metadata = await getContractMetadata({ contract });
metadata // Record<string, any>
metadata.name; // string
metadata.symbol; // string
```

after:
```ts
const metadata = await getContractMetadata({ contract });
metadata // Record<string, unknown>
metadata.name; // string | null
metadata.symbol; // string | null
```


Metadata is not (and was never) strictly defined outside of `name` and `symbol` and may contain any type of data in the record.
This is not a runtime change but it may break type inference in existing apps that relied on the previous return type.

**Recommended fix:**
You *should* type-guard any key you access from "metadata".
```ts
const metadata = await getContractMetadata({ contract });
if ("foo" in metadata && typeof metadata.foo === "string") {
metadata.foo; // string
}
```

**Quick fix:**
If adding type assertions is not something you can do in the short term you can also assert the type directly.
_This is as "unsafe" as the type was before._

```ts
const metadata = await getContractMetadata({ contract });
const foo = metadata.foo as string;
```

2 changes: 1 addition & 1 deletion apps/dashboard/src/@/api/universal-bridge/token-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function getUniversalBridgeTokens(props: {
headers: {
"Content-Type": "application/json",
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
} as Record<string, string>,
},
method: "GET",
});

Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/@/api/universal-bridge/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function addUniversalBridgeTokenRoute(props: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.project.publishableKey,
} as Record<string, string>,
},
method: "POST",
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ export async function fetchDashboardContractMetadata(

return {
contractType: compilerMetadata?.name || "",
image: contractMetadata.image || "",
image:
contractMetadata.image && typeof contractMetadata.image === "string"
? contractMetadata.image
: "",
name: contractName,
symbol: contractSymbol,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import * as ERC20Ext from "thirdweb/extensions/erc20";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import { download } from "thirdweb/storage";
import type { OverrideEntry } from "thirdweb/utils";
import { maxUint256 } from "thirdweb/utils";
import { isRecord, maxUint256, type OverrideEntry } from "thirdweb/utils";
import type { z } from "zod";
import type {
ClaimCondition as LegacyClaimCondition,
Expand Down Expand Up @@ -86,7 +85,7 @@ export async function getClaimPhasesInLegacyFormat(
]);
const snapshot = await fetchSnapshot(
condition.merkleRoot,
contractMetadata.merkle,
isRecord(contractMetadata.merkle) ? contractMetadata.merkle : {},
options.contract.client,
);
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ export default async function Image({
chainName: info.chainMetadata.name,
contractAddress: info.serverContract.address,
displayName: contractDisplayName,
logo: contractMetadata.image,
logo:
contractMetadata.image && typeof contractMetadata.image === "string"
? contractMetadata.image
: undefined,
});
} catch {
return contractOGImageTemplate({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,31 @@ export async function ERC20PublicPage(props: {
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={contractCreator}
image={contractMetadata.image}
image={
contractMetadata.image &&
typeof contractMetadata.image === "string"
? contractMetadata.image
: undefined
}
isDashboardUser={isDashboardUser}
name={contractMetadata.name}
name={
contractMetadata.name && typeof contractMetadata.name === "string"
? contractMetadata.name
: // if we do not have a contract name fall back to the address
props.clientContract.address
}
socialUrls={
typeof contractMetadata.social_urls === "object" &&
contractMetadata.social_urls !== null
? contractMetadata.social_urls
: {}
}
symbol={contractMetadata.symbol}
symbol={
contractMetadata.symbol &&
typeof contractMetadata.symbol === "string"
? contractMetadata.symbol
: undefined
}
/>
</div>
</div>
Expand Down Expand Up @@ -143,21 +158,25 @@ export async function ERC20PublicPage(props: {
</div>
)}

{showBuyEmbed && (
<div className="container max-w-7xl pb-10">
<GridPatternEmbedContainer>
<BuyEmbed
chainMetadata={props.chainMetadata}
claimConditionMeta={claimConditionMeta}
clientContract={props.clientContract}
tokenAddress={props.clientContract.address}
tokenDecimals={tokenDecimals}
tokenName={contractMetadata.name}
tokenSymbol={contractMetadata.symbol}
/>
</GridPatternEmbedContainer>
</div>
)}
{showBuyEmbed &&
contractMetadata.name &&
typeof contractMetadata.name === "string" &&
contractMetadata.symbol &&
typeof contractMetadata.symbol === "string" && (
<div className="container max-w-7xl pb-10">
<GridPatternEmbedContainer>
<BuyEmbed
chainMetadata={props.chainMetadata}
claimConditionMeta={claimConditionMeta}
clientContract={props.clientContract}
tokenAddress={props.clientContract.address}
tokenDecimals={tokenDecimals}
tokenName={contractMetadata.name}
tokenSymbol={contractMetadata.symbol}
/>
</GridPatternEmbedContainer>
</div>
)}

<div className="container flex max-w-7xl grow flex-col pb-10">
<div className="flex grow flex-col gap-8">
Expand All @@ -167,12 +186,15 @@ export async function ERC20PublicPage(props: {
contractAddress={props.clientContract.address}
/>

<RecentTransfers
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
decimals={tokenDecimals}
tokenSymbol={contractMetadata.symbol}
/>
{contractMetadata.symbol &&
typeof contractMetadata.symbol === "string" && (
<RecentTransfers
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
decimals={tokenDecimals}
tokenSymbol={contractMetadata.symbol}
/>
)}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ export async function NFTPublicPage(props: {
const _isTokenByIndexSupported =
props.type === "erc721" && isTokenByIndexSupported(functionSelectors);

// FIXME: this is technically a bad fallback but we gotta do what we gotta do
const contractMetadataWithNameAndSymbolFallback = {
...contractMetadata,
// fall back to the contract address if the name is not set
name: contractMetadata.name || props.clientContract.address,
symbol: contractMetadata.symbol || "",
};
const buyNFTDropCard = nftDropClaimParams ? (
<BuyNFTDropCardServer
chainMetadata={props.chainMetadata}
Expand All @@ -73,7 +80,7 @@ export async function NFTPublicPage(props: {
<NFTsGrid
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
collectionMetadata={contractMetadata}
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
gridClassName={
buyNFTDropCard
? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
Expand Down Expand Up @@ -110,7 +117,7 @@ export async function NFTPublicPage(props: {
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
contractCreator={contractCreator}
contractMetadata={contractMetadata}
contractMetadata={contractMetadataWithNameAndSymbolFallback}
isDashboardUser={isDashboardUser}
>
<ResponsiveLayout
Expand Down Expand Up @@ -138,7 +145,7 @@ export async function NFTPublicPage(props: {
<PageLoadTokenViewerSheet
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
collectionMetadata={contractMetadata}
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
tokenByIndexSupported={_isTokenByIndexSupported}
tokenId={BigInt(props.tokenId)}
type={props.type}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function TokenViewerSheet(
clientContract: ThirdwebContract;
chainMetadata: ChainMetadata;
collectionMetadata: {
name: string;
name: string | null;
image?: string;
};
type: "erc1155" | "erc721";
Expand All @@ -57,6 +57,11 @@ export function TokenViewerSheet(
) {
const tokenId = props.variant === "fetch-data" ? props.tokenId : props.nft.id;

const collectionMetadataWithNameFallback = {
...props.collectionMetadata,
name: props.collectionMetadata.name || props.clientContract.address,
};

return (
<Dialog
onOpenChange={(open) => {
Expand All @@ -78,15 +83,15 @@ export function TokenViewerSheet(
<FetchAndRenderTokenInfo
chainMetadata={props.chainMetadata}
clientContract={props.clientContract}
collectionMetadata={props.collectionMetadata}
collectionMetadata={collectionMetadataWithNameFallback}
tokenByIndexSupported={props.tokenByIndexSupported}
tokenId={props.tokenId}
type={props.type}
/>
) : (
<TokenInfoUI
chainMetadata={props.chainMetadata}
collectionMetadata={props.collectionMetadata}
collectionMetadata={collectionMetadataWithNameFallback}
contract={props.clientContract}
data={props.nft}
tokenId={tokenId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ export const SettingsMetadata = ({
console.error(err);
}
}
let image: string | undefined = metadata.data?.image;
let image: string | undefined =
metadata.data?.image && typeof metadata.data.image === "string"
? metadata.data.image
: undefined;
try {
image = image
? // eslint-disable-next-line no-restricted-syntax
Expand Down
4 changes: 3 additions & 1 deletion apps/dashboard/src/app/(app)/drops/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export default async function DropPage({
]);

const thumbnail =
project.thumbnail || nft.metadata.image || contractMetadata.image || "";
project.thumbnail ||
nft.metadata.image ||
(typeof contractMetadata.image === "string" ? contractMetadata.image : "");

const displayName = contractMetadata.name || nft.metadata.name || "";

Expand Down
6 changes: 3 additions & 3 deletions packages/service-utils/src/core/get-auth-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ export function getAuthHeaders(
case !!secretKey:
return {
"x-secret-key": secretKey,
} as Record<string, string>;
};

// 2. if we have a JWT AND either a teamId or clientId, we'll use the JWT for auth
case !!(jwt && (teamId || clientId)):
return {
Authorization: `Bearer ${jwt}`,
} as Record<string, string>;
};

// 3. if we have an incoming service api key, we'll use it
case !!incomingServiceApiKey: {
return {
"x-service-api-key": incomingServiceApiKey,
} as Record<string, string>;
};
}

// 4. if nothing else is present, we'll use the service api key
Expand Down
5 changes: 5 additions & 0 deletions packages/thirdweb/src/exports/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,8 @@ export type { JWTPayload } from "../utils/jwt/types.js";
export type { NFTInput, NFTMetadata } from "../utils/nft/parseNft.js";
export { resolvePromisedValue } from "../utils/promise/resolve-promised-value.js";
export { shortenLargeNumber } from "../utils/shortenLargeNumber.js";

// ------------------------------------------------
// type guards
// ------------------------------------------------
export * from "../utils/type-guards.js";
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { upload } from "../../../storage/upload.js";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { isRecord } from "../../../utils/type-guards.js";
import { setContractURI } from "../../common/__generated__/IContractMetadata/write/setContractURI.js";

/**
Expand Down Expand Up @@ -80,7 +81,12 @@

// keep the old merkle roots from other tokenIds
for (const key of Object.keys(metadata.merkle || {})) {
merkleInfos[key] = metadata.merkle[key];
const merkleInfo = isRecord(metadata.merkle)
? metadata.merkle[key]
: undefined;
if (merkleInfo) {
merkleInfos[key] = merkleInfo;
}

Check warning on line 89 in packages/thirdweb/src/extensions/airdrop/write/saveSnapshot.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/extensions/airdrop/write/saveSnapshot.ts#L84-L89

Added lines #L84 - L89 were not covered by tests
}
const mergedMetadata = {
...metadata,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,9 @@ export { isContractURISupported as isGetContractMetadataSupported } from "../__g
export async function getContractMetadata(
options: BaseTransactionOptions,
): Promise<{
name: string;
symbol: string;
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix any
[key: string]: any;
name: string | null;
symbol: string | null;
[key: string]: unknown;
}> {
const [resolvedMetadata, resolvedName, resolvedSymbol] = await Promise.all([
contractURI(options)
Expand All @@ -43,8 +42,14 @@ export async function getContractMetadata(

// TODO: basic parsing?
return {
...resolvedMetadata,
name: resolvedMetadata?.name ?? resolvedName,
symbol: resolvedMetadata?.symbol ?? resolvedSymbol,
...(resolvedMetadata ?? {}),
name:
resolvedMetadata?.name && typeof resolvedMetadata.name === "string"
? resolvedMetadata.name
: resolvedName,
symbol:
resolvedMetadata?.symbol && typeof resolvedMetadata.symbol === "string"
? resolvedMetadata.symbol
: resolvedSymbol,
};
}
Loading
Loading