Compare commits

..
Author SHA1 Message Date
Joaquim VergesandGitHub 8ee427d590 Version Packages (#6767) 2025-04-18 11:34:20 +02:00
MananTank 24319f4570 [TOOL-4175] Dashboard: Add buy listing button in listing drawer in marketplace (#6757)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces the `BuyDirectListingButton` component into the `ListingDrawer` component, enhancing the marketplace functionality by allowing users to buy listings directly. It also updates documentation and adds error handling for the transaction process.

### Detailed summary
- Updated documentation for the `BuyDirectListingButton` to specify `BuyDirectListingButtonProps`.
- Imported `BuyDirectListingButton` in `listing-drawer.tsx`.
- Implemented the `BuyDirectListingButton` in the `ListingDrawer` component.
- Added error and success toast notifications for transaction outcomes.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-04-18 09:15:29 +00:00
Joaquim VergesandGitHub f38b262cd0 [SDK] Refactor ERC721 getNFTs to handle token ID offsets correctly (#6768) 2025-04-18 02:14:19 -07:00
jnsdls 5b35a6227b Update RPC usage charts to handle string values and improve visualization (#6766)
# Update RPC Usage Dashboard with Enhanced Visualization

This PR updates the RPC usage dashboard to improve data visualization and fix type issues:

- Changed numeric types in API response to strings for better precision
- Added peak RPS data to the rate graph for more comprehensive monitoring
- Implemented a "maxLine" feature to replace the previous maxLimit approach for clearer rate limit visualization
- Added support for both stacked and individual chart variants
- Fixed date formatting by properly handling UTC timezone with 'Z' suffix
- Updated cache key to "rpc-usage-last-24-hours:v2" to ensure fresh data after changes
- Removed unnecessary CardFooter component and simplified chart rendering logic
- Enhanced tooltip display for better readability

These changes provide users with more accurate and detailed information about their RPC usage patterns.
2025-04-18 08:55:00 +00:00
Joaquim VergesandGitHub 03e238515b [SDK] Improve NFT data fallbacks for indexer cache misses (#6765) 2025-04-18 01:49:58 -07:00
Jonas DanielsandGitHub 72875f9de5 Add support for service-to-service API key authentication (#6764) 2025-04-18 10:43:52 +02:00
jnsdls 97ce74f307 Add universal bridge landing pages to framer-rewrites (#6763)
### TL;DR

Added URL rewrites for Universal Bridge landing pages and Enterprise page.

### What changed?

Added two new URL paths to the framer-rewrites.js file:
- `/universal-bridge-regions/:region_slug` for region-specific Universal Bridge landing pages
- `/enterprise` for the Enterprise page

### How to test?

1. Navigate to `/universal-bridge-regions/[any-region-slug]` and verify the page loads correctly
2. Navigate to `/enterprise` and verify the page loads correctly
3. Ensure both pages render with the expected content and styling

### Why make this change?

These URL paths need to be included in the framer-rewrites to ensure proper routing and rendering of the Universal Bridge landing pages by region and the Enterprise page.
2025-04-18 08:06:49 +00:00
Joaquim VergesandGitHub 3a43fb23a4 [Build] Add insight package to turbo run commands (#6762) 2025-04-17 18:59:33 -07:00
31 changed files with 303 additions and 185 deletions
+3
View File
@@ -58,4 +58,7 @@ module.exports = [
"/faucets",
// -- brand kit --
"/brand-kit",
// -- universal bridge landing pages --
"/universal-bridge-regions/:region_slug",
"/enterprise",
];
+10 -9
View File
@@ -56,19 +56,20 @@ export const fetchRPCUsage = unstable_cache(
type Last24HoursRPCUsageApiResponse = {
peakRate: {
date: string;
peakRPS: number;
peakRPS: string;
};
averageRate: {
date: string;
averageRate: number;
includedCount: number;
rateLimitedCount: number;
overageCount: number;
averageRate: string;
peakRPS: string;
includedCount: string;
rateLimitedCount: string;
overageCount: string;
}[];
totalCounts: {
includedCount: number;
rateLimitedCount: number;
overageCount: number;
includedCount: string;
rateLimitedCount: string;
overageCount: string;
};
};
@@ -106,7 +107,7 @@ export const getLast24HoursRPCUsage = unstable_cache(
data: resData.data as Last24HoursRPCUsageApiResponse,
};
},
["rpc-usage-last-24-hours"],
["rpc-usage-last-24-hours:v2"],
{
revalidate: 60, // 1 minute
},
@@ -4,7 +4,6 @@ import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
@@ -31,18 +30,19 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
description?: string;
titleClassName?: string;
};
footer?: React.ReactNode;
customHeader?: React.ReactNode;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
showLegend?: boolean;
maxLimit?: number;
yAxis?: boolean;
xAxis?: {
sameDay?: boolean;
};
variant?: "stacked" | "individual";
// chart className
chartClassName?: string;
isPending: boolean;
@@ -77,17 +77,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
) : props.data.length === 0 ? (
<EmptyChartState />
) : (
<AreaChart
accessibilityLayer
data={
props.maxLimit
? props.data.map((d) => ({
...d,
maxLimit: props.maxLimit,
}))
: props.data
}
>
<AreaChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
{props.yAxis && <YAxis tickLine={false} axisLine={false} />}
<XAxis
@@ -136,26 +126,28 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
</linearGradient>
))}
</defs>
{configKeys.map((key) => (
<Area
key={key}
dataKey={key}
type="natural"
fill={`url(#fill_${key})`}
fillOpacity={0.4}
stroke={`var(--color-${key})`}
stackId="a"
/>
))}
{props.maxLimit && (
<Area
type="monotone"
dataKey="maxLimit"
stroke="#ef4444"
strokeWidth={2}
strokeDasharray="5 5"
fill="none"
/>
{configKeys.map((key) =>
key === "maxLine" ? (
<Area
key={key}
type="monotone"
dataKey="maxLine"
stroke="#ef4444"
strokeWidth={2}
strokeDasharray="5 5"
fill="none"
/>
) : (
<Area
key={key}
dataKey={key}
type="natural"
fill={`url(#fill_${key})`}
fillOpacity={0.4}
stroke={`var(--color-${key})`}
stackId={props.variant !== "stacked" ? undefined : "a"}
/>
),
)}
{props.showLegend && (
@@ -167,9 +159,6 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
)}
</ChartContainer>
</CardContent>
{props.footer && (
<CardFooter className="w-full">{props.footer}</CardFooter>
)}
</Card>
);
}
+1 -3
View File
@@ -243,9 +243,7 @@ const ChartTooltipContent = React.forwardRef<
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{item.name === "maxLimit"
? "Upper Limit"
: itemConfig?.label || item.name}
{itemConfig?.label || item.name}
</span>
</div>
{item.value !== undefined && (
@@ -6,6 +6,7 @@ import {
import {
THIRDWEB_BUNDLER_DOMAIN,
THIRDWEB_INAPP_WALLET_DOMAIN,
THIRDWEB_INSIGHT_API_DOMAIN,
THIRDWEB_PAY_DOMAIN,
THIRDWEB_RPC_DOMAIN,
THIRDWEB_SOCIAL_API_DOMAIN,
@@ -32,6 +33,7 @@ export function getThirdwebClient(jwt?: string) {
storage: THIRDWEB_STORAGE_DOMAIN,
social: THIRDWEB_SOCIAL_API_DOMAIN,
bundler: THIRDWEB_BUNDLER_DOMAIN,
insight: THIRDWEB_INSIGHT_API_DOMAIN,
});
}
@@ -10,10 +10,11 @@ import type {
DirectListing,
EnglishAuction,
} from "thirdweb/extensions/marketplace";
import { useActiveAccount } from "thirdweb/react";
import { BuyDirectListingButton, useActiveAccount } from "thirdweb/react";
import { CodeClient } from "@/components/ui/code/code.client";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { toast } from "sonner";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import { CancelTab } from "./cancel-tab";
import { LISTING_STATUS } from "./types";
@@ -141,11 +142,6 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
</GridItem>
</>
)}
{/*
Todo: Add a Buy button somewhere in this section once the Dashboard is fully migrated to v5 (?)
Kien already shipped a prebuilt component for the Marketplace Buy Button in SDK v5
*/}
</SimpleGrid>
</Card>
{data?.asset.metadata.properties ? (
@@ -168,6 +164,27 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
twAccount={twAccount}
/>
)}
{!isOwner && renderData.status === "ACTIVE" && (
<BuyDirectListingButton
listingId={renderData.id}
contractAddress={renderData.assetContractAddress}
chain={contract.chain}
client={contract.client}
className="w-full"
quantity={1n}
onError={(error) => {
toast.error("Failed to buy listing", {
description: error.message,
});
}}
onTransactionConfirmed={() => {
toast.success("Listing bought successfully");
}}
>
Buy Listing
</BuyDirectListingButton>
)}
</SheetContent>
</Sheet>
);
@@ -8,18 +8,20 @@ export function CountGraph(props: {
currentRateLimit: number;
data: {
date: string;
includedCount: number;
overageCount: number;
rateLimitedCount: number;
includedCount: string;
overageCount: string;
rateLimitedCount: string;
}[];
}) {
const hasAnyRateLimited = props.data.some((v) => v.rateLimitedCount > 0);
const hasAnyRateLimited = props.data.some(
(v) => Number(v.rateLimitedCount) > 0,
);
return (
<ThirdwebAreaChart
chartClassName="aspect-[1.5] lg:aspect-[4]"
header={{
title: "Requests Over Time",
description: "Requests over the last 24 hours. All times in UTC.",
description: "Requests over the last 24 hours.",
}}
config={
hasAnyRateLimited
@@ -47,13 +49,13 @@ export function CountGraph(props: {
}}
hideLabel={false}
toolTipLabelFormatter={(label) => {
return formatDate(new Date(label), "MMM dd, HH:mm");
return formatDate(label, "MMM dd, HH:mm");
}}
// @ts-expect-error - sending MORE data than expected is ok
data={props.data
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
.map((v) => ({
time: v.date,
time: `${v.date}Z`,
includedCount: Number(v.includedCount) + Number(v.overageCount),
rateLimitedCount: Number(v.rateLimitedCount),
}))}
@@ -2,54 +2,63 @@
import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
import { formatDate } from "date-fns";
import { InfoIcon } from "lucide-react";
export function RateGraph(props: {
peakPercentage: number;
currentRateLimit: number;
data: { date: string; averageRate: number }[];
data: { date: string; averageRate: string; peakRPS: string }[];
}) {
return (
<ThirdwebAreaChart
chartClassName="aspect-[1.5] lg:aspect-[4]"
header={{
title: "Request Rate Over Time",
description: "Request rate over the last 24 hours. All times in UTC.",
description: "Request rate over the last 24 hours.",
}}
// only show the footer if the peak usage is greater than 80%
footer={
props.peakPercentage > 80 ? (
<div className="flex items-center justify-center gap-2">
<InfoIcon className="h-4 w-4 text-muted-foreground" />
<p className="text-muted-foreground text-xs">
The red dashed line represents your current plan rate limit (
{props.currentRateLimit} RPS)
</p>
</div>
) : undefined
config={
props.peakPercentage > 80
? {
averageRate: {
label: "Average RPS",
color: "hsl(var(--chart-1))",
},
peakRPS: {
label: "Peak RPS",
color: "hsl(var(--chart-2))",
},
maxLine: {
label: "Plan Rate Limit",
},
}
: {
averageRate: {
label: "Average RPS",
color: "hsl(var(--chart-1))",
},
peakRPS: {
label: "Peak RPS",
color: "hsl(var(--chart-2))",
},
}
}
config={{
averageRate: {
label: "Average RPS",
color: "hsl(var(--chart-1))",
},
}}
// @ts-expect-error - maxLine is always sent but not always rendered, this is OK
data={props.data
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
.map((v) => ({
time: v.date,
averageRate: Number(v.averageRate.toFixed(2)),
time: `${v.date}Z`,
averageRate: Number(Number(v.averageRate).toFixed(2)),
peakRPS: Number(v.peakRPS),
maxLine: props.currentRateLimit,
}))}
yAxis
xAxis={{
sameDay: true,
}}
showLegend
hideLabel={false}
toolTipLabelFormatter={(label) => {
return formatDate(new Date(label), "MMM dd, HH:mm");
return formatDate(label, "MMM dd, HH:mm");
}}
// only show the upper limit if the peak usage is greater than 80%
maxLimit={props.peakPercentage > 80 ? props.currentRateLimit : undefined}
isPending={false}
/>
);
@@ -48,7 +48,7 @@ export default async function RPCUsage(props: {
const { peakRate, totalCounts, averageRate } = apiData.data;
// Calculate percentage of limit for the peak
const peakPercentage = (peakRate.peakRPS / currentRateLimit) * 100;
const peakPercentage = (Number(peakRate.peakRPS) / currentRateLimit) * 100;
// Determine status based on peak percentage
const getStatusColor = (percentage: number) => {
@@ -98,7 +98,7 @@ export default async function RPCUsage(props: {
<CardContent>
<div className="flex items-center justify-between">
<div className="font-bold text-2xl">
{peakRate.peakRPS.toFixed(1)} RPS
{Number(peakRate.peakRPS)} RPS
</div>
<div className="flex items-center gap-2">
<div
@@ -114,7 +114,7 @@ export default async function RPCUsage(props: {
<p className="mt-1 text-muted-foreground text-xs">
<ClockIcon className="mr-1 inline h-3 w-3" />
{peakRate.date
? format(new Date(peakRate.date), "MMM d, HH:mm")
? format(new Date(`${peakRate.date}Z`), "MMM d, HH:mm")
: "No Requests in last 24 hours"}
</p>
</CardContent>
@@ -177,9 +177,9 @@ export default async function RPCUsage(props: {
<AlertTriangleIcon className="h-4 w-4" />
<AlertTitle>Rate Limit Exceeded</AlertTitle>
<AlertDescription>
Your peak usage of {peakRate.peakRPS.toFixed(1)} RPS has exceeded
your plan limit of {currentRateLimit} RPS. Consider upgrading your
plan to avoid rate limiting.
Your peak usage of {Number(peakRate.peakRPS)} RPS has exceeded your
plan limit of {currentRateLimit} RPS. Consider upgrading your plan
to avoid rate limiting.
</AlertDescription>
</Alert>
)}
+3
View File
@@ -20,3 +20,6 @@ export const THIRDWEB_SOCIAL_API_DOMAIN =
export const THIRDWEB_BUNDLER_DOMAIN =
process.env.NEXT_PUBLIC_BUNDLER_URL || "bundler.thirdweb-dev.com";
export const THIRDWEB_INSIGHT_API_DOMAIN =
process.env.NEXT_PUBLIC_INSIGHT_API_URL || "insight.thirdweb-dev.com";
+5 -5
View File
@@ -16,13 +16,13 @@
"storybook:react": "turbo run storybook --filter=./packages/*",
"prefix": "pnpm dlx sherif@latest -i remark-gfm -i eslint --fix",
"fix": "turbo run fix",
"wallet-ui": "turbo run dev --filter=./apps/wallet-ui --filter=./packages/thirdweb",
"wallet-ui:build": "turbo run build --filter=./apps/wallet-ui --filter=./packages/thirdweb",
"playground": "turbo run dev --filter=./apps/playground-web --filter=./packages/thirdweb",
"wallet-ui": "turbo run dev --filter=./apps/wallet-ui --filter=./packages/thirdweb --filter=./packages/insight",
"wallet-ui:build": "turbo run build --filter=./apps/wallet-ui --filter=./packages/thirdweb --filter=./packages/insight",
"playground": "turbo run dev --filter=./apps/playground-web --filter=./packages/thirdweb --filter=./packages/insight",
"playground:build": "turbo run build --filter=./apps/playground-web",
"portal": "turbo run dev --filter=./apps/portal --filter=./packages/thirdweb",
"portal": "turbo run dev --filter=./apps/portal --filter=./packages/thirdweb --filter=./packages/insight",
"portal:build": "turbo run build --filter=./apps/portal",
"dashboard": "turbo run dev --filter=./apps/dashboard --filter=./packages/thirdweb",
"dashboard": "turbo run dev --filter=./apps/dashboard --filter=./packages/thirdweb --filter=./packages/insight",
"dashboard:build": "turbo run build --filter=./apps/dashboard",
"build": "turbo run build --filter=./packages/*",
"build:release": "turbo run build --filter=./packages/* --force",
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/service-utils
## 0.9.3
### Patch Changes
- [#6764](https://github.com/thirdweb-dev/js/pull/6764) [`72875f9`](https://github.com/thirdweb-dev/js/commit/72875f9de5041a6423cfa3ac347bb033e3fbf523) Thanks [@jnsdls](https://github.com/jnsdls)! - add support for service-to-service api key authentication
## 0.9.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/service-utils",
"version": "0.9.2",
"version": "0.9.3",
"type": "module",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
@@ -147,7 +147,18 @@ export async function extractAuthorizationData(
}
}
let incomingServiceApiKey: string | null = null;
let incomingServiceApiKeyHash: string | null = null;
if (headers.has("x-service-api-key")) {
incomingServiceApiKey = headers.get("x-service-api-key");
if (incomingServiceApiKey) {
incomingServiceApiKeyHash = await hashSecretKey(incomingServiceApiKey);
}
}
return {
incomingServiceApiKey,
incomingServiceApiKeyHash,
jwt,
hashedJWT: jwt ? await hashSecretKey(jwt) : null,
secretKey,
+8 -2
View File
@@ -228,7 +228,7 @@ export async function fetchTeamAndProject(
config: CoreServiceConfig,
): Promise<ApiResponse> {
const { apiUrl, serviceApiKey } = config;
const { teamId, clientId } = authData;
const { teamId, clientId, incomingServiceApiKey } = authData;
const url = new URL("/v2/keys/use", apiUrl);
if (clientId) {
@@ -247,11 +247,17 @@ export async function fetchTeamAndProject(
headers: {
...(authData.secretKey ? { "x-secret-key": authData.secretKey } : {}),
...(authData.jwt ? { Authorization: `Bearer ${authData.jwt}` } : {}),
"x-service-api-key": serviceApiKey,
// use the incoming service api key if it exists, otherwise use the service api key
// this is done to ensure that the incoming service API key is VALID in the first place
"x-service-api-key": incomingServiceApiKey
? incomingServiceApiKey
: serviceApiKey,
"content-type": "application/json",
},
});
// TODO: if the response is a well understood status code (401, 402, etc), we should skip retry logic
let text = "";
try {
text = await response.text();
@@ -19,6 +19,8 @@ describe("authorizeClient", () => {
secretKeyHash: null,
hashedJWT: null,
jwt: null,
incomingServiceApiKey: null,
incomingServiceApiKeyHash: null,
ecosystemId: null,
ecosystemPartnerId: null,
},
@@ -10,6 +10,7 @@ describe("authorizeClient", () => {
secretKeyHash: "secret-hash",
bundleId: null,
origin: "example.com",
incomingServiceApiKey: null,
};
it("should authorize client with valid secret key", () => {
@@ -27,6 +28,7 @@ describe("authorizeClient", () => {
secretKeyHash: null,
bundleId: null,
origin: "sub.example.com",
incomingServiceApiKey: null,
};
const result = authorizeClient(
@@ -43,6 +45,7 @@ describe("authorizeClient", () => {
secretKeyHash: null,
bundleId: null,
origin: null,
incomingServiceApiKey: null,
};
const validProjectResponseAnyDomain = {
@@ -67,6 +70,7 @@ describe("authorizeClient", () => {
secretKeyHash: null,
bundleId: "com.foo.bar",
origin: null,
incomingServiceApiKey: null,
};
const result = authorizeClient(
@@ -87,6 +91,7 @@ describe("authorizeClient", () => {
secretKeyHash: null,
bundleId: null,
origin: "unauthorized.com",
incomingServiceApiKey: null,
};
const result = authorizeClient(
@@ -101,4 +106,21 @@ describe("authorizeClient", () => {
expect(result.errorCode).toBe("ORIGIN_UNAUTHORIZED");
expect(result.status).toBe(401);
});
it("should authorize client with incoming service api key", () => {
const authOptionsWithServiceKey: ClientAuthorizationPayload = {
secretKeyHash: null,
bundleId: null,
origin: "unauthorized.com", // Even unauthorized origin should work with service key
incomingServiceApiKey: "test-service-key",
};
const result = authorizeClient(
authOptionsWithServiceKey,
validTeamAndProjectResponse,
// biome-ignore lint/suspicious/noExplicitAny: test only
) as any;
expect(result.authorized).toBe(true);
expect(result.project).toEqual(validTeamAndProjectResponse.project);
});
});
@@ -5,13 +5,14 @@ export type ClientAuthorizationPayload = {
secretKeyHash: string | null;
bundleId: string | null;
origin: string | null;
incomingServiceApiKey: string | null;
};
export function authorizeClient(
authOptions: ClientAuthorizationPayload,
teamAndProjectResponse: TeamAndProjectResponse,
): AuthorizationResult {
const { origin, bundleId } = authOptions;
const { origin, bundleId, incomingServiceApiKey } = authOptions;
const { team, project, authMethod } = teamAndProjectResponse;
const authResult: AuthorizationResult = {
@@ -31,6 +32,12 @@ export function authorizeClient(
return authResult;
}
if (authMethod === "publishableKey" && incomingServiceApiKey) {
// if the auth was done using a combination of publishableKey and incomingServiceKey,
// we will treat this the same as a secret key auth (relying on the upstream service to have already validated the publishableKey)
return authResult;
}
// check for public restrictions
if (project.domains.includes("*")) {
return authResult;
@@ -9,6 +9,8 @@ import { authorizeService } from "./service.js";
import type { AuthorizationResult } from "./types.js";
export type AuthorizationInput = {
incomingServiceApiKey: string | null;
incomingServiceApiKeyHash: string | null;
secretKey: string | null;
clientId: string | null;
ecosystemId: string | null;
@@ -45,13 +47,19 @@ export async function authorize(
let teamAndProjectResponse: TeamAndProjectResponse | null = null;
// Use a separate cache key per auth method.
const cacheKey = authData.secretKeyHash
? `key-v2:secret-key:${authData.secretKeyHash}`
: authData.hashedJWT
? `key-v2:dashboard-jwt:${authData.hashedJWT}:${authData.teamId ?? "default"}`
: authData.clientId
? `key-v2:client-id:${authData.clientId}`
: null;
const cacheKey = authData.incomingServiceApiKey
? // incoming service key + clientId case
`key-v2:service-key:${authData.incomingServiceApiKeyHash}:${authData.clientId ?? "default"}`
: authData.secretKeyHash
? // secret key case
`key-v2:secret-key:${authData.secretKeyHash}`
: authData.hashedJWT
? // dashboard jwt case
`key-v2:dashboard-jwt:${authData.hashedJWT}:${authData.teamId ?? "default"}`
: authData.clientId
? // clientId case
`key-v2:client-id:${authData.clientId}`
: null;
// TODO if we have cache options we want to check the cache first
if (cacheOptions && cacheKey) {
+11
View File
@@ -151,7 +151,18 @@ export function extractAuthorizationData(
}
}
let incomingServiceApiKey: string | null = null;
let incomingServiceApiKeyHash: string | null = null;
if (getHeader(headers, "x-service-api-key")) {
incomingServiceApiKey = getHeader(headers, "x-service-api-key");
if (incomingServiceApiKey) {
incomingServiceApiKeyHash = hashSecretKey(incomingServiceApiKey);
}
}
return {
incomingServiceApiKey,
incomingServiceApiKeyHash,
jwt,
hashedJWT: jwt ? hashSecretKey(jwt) : null,
secretKeyHash,
+6
View File
@@ -1,5 +1,11 @@
# thirdweb
## 5.95.1
### Patch Changes
- [#6765](https://github.com/thirdweb-dev/js/pull/6765) [`03e2385`](https://github.com/thirdweb-dev/js/commit/03e238515b59a0934f94c9a5d7b92c35d5dc0b5b) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Better fallbacks for live NFT data cache miss
## 5.95.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "thirdweb",
"version": "5.95.0",
"version": "5.95.1",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
@@ -50,7 +50,6 @@ export async function getNFT(
async function getNFTFromInsight(
options: BaseTransactionOptions<GetNFTParams>,
): Promise<NFT> {
const tokenId = options.tokenId;
const nft = await getNFTInsight({
client: options.contract.client,
chain: options.contract.chain,
@@ -58,22 +57,8 @@ async function getNFTFromInsight(
tokenId: options.tokenId,
});
if (!nft) {
return parseNFT(
{
id: tokenId,
type: "ERC1155",
uri: "",
},
{
tokenId: options.tokenId,
tokenUri: "",
type: "ERC1155",
owner: null,
supply: 0n,
tokenAddress: options.contract.address,
chainId: options.contract.chain.id,
},
);
// fresh contracts might be delayed in indexing, so we fallback to RPC
return getNFTFromRPC(options);
}
return nft;
}
@@ -65,16 +65,28 @@ async function getNFTsFromInsight(
): Promise<NFT[]> {
const { contract, start, count = Number(DEFAULT_QUERY_ALL_COUNT) } = options;
const result = await getContractNFTs({
client: contract.client,
chains: [contract.chain],
contractAddress: contract.address,
queryOptions: {
limit: count,
page: start ? Math.floor(start / count) : undefined,
},
});
const [result, supply] = await Promise.all([
getContractNFTs({
client: contract.client,
chains: [contract.chain],
contractAddress: contract.address,
queryOptions: {
limit: count,
page: start ? Math.floor(start / count) : undefined,
},
}),
nextTokenIdToMint(options).catch(() => maxUint256),
]);
const currentOffset = start ?? 0;
const expectedResultLength = Math.min(
count,
Math.max(0, Number(supply) - currentOffset),
);
if (result.length < expectedResultLength) {
// fresh contracts might be delayed in indexing, so we fallback to RPC
return getNFTsFromRPC(options);
}
return result;
}
@@ -95,6 +107,7 @@ async function getNFTsFromRPC(
getNFT({
...options,
tokenId: i,
useIndexer: false,
}),
);
}
@@ -97,7 +97,9 @@ async function getOwnedNFTsFromRPC(
const ownedBalances = await getOwnedTokenIds(options);
const nfts = await Promise.all(
ownedBalances.map((ob) => getNFT({ ...options, tokenId: ob.tokenId })),
ownedBalances.map((ob) =>
getNFT({ ...options, tokenId: ob.tokenId, useIndexer: false }),
),
);
return nfts.map((nft, index) => ({
@@ -78,7 +78,6 @@ export async function getNFT(
async function getNFTFromInsight(
options: BaseTransactionOptions<GetNFTParams>,
): Promise<NFT> {
const tokenId = options.tokenId;
const nft = await getNFTInsight({
client: options.contract.client,
chain: options.contract.chain,
@@ -87,21 +86,8 @@ async function getNFTFromInsight(
includeOwners: options.includeOwner,
});
if (!nft) {
return parseNFT(
{
id: tokenId,
type: "ERC721",
uri: "",
},
{
tokenId,
tokenUri: "",
type: "ERC721",
owner: null,
tokenAddress: options.contract.address,
chainId: options.contract.chain.id,
},
);
// fresh contracts might be delayed in indexing, so we fallback to RPC
return getNFTFromRPC(options);
}
return nft;
}
@@ -104,16 +104,37 @@ async function getNFTsFromInsight(
): Promise<NFT[]> {
const { contract, start, count = Number(DEFAULT_QUERY_ALL_COUNT) } = options;
const result = await getContractNFTs({
client: contract.client,
chains: [contract.chain],
contractAddress: contract.address,
includeOwners: options.includeOwners ?? false,
queryOptions: {
limit: count,
page: start ? Math.floor(start / count) : undefined,
},
});
const [result, supplyInfo] = await Promise.all([
getContractNFTs({
client: contract.client,
chains: [contract.chain],
contractAddress: contract.address,
includeOwners: options.includeOwners ?? false,
queryOptions: {
limit: count,
page: start ? Math.floor(start / count) : undefined,
},
}),
getSupplyInfo(options).catch(() => ({
maxSupply: 0,
startTokenId: 0,
})),
]);
const currentOffset = start ?? 0;
const expectedResultLength = Math.min(
count,
Math.max(
0,
Number(supplyInfo.maxSupply) -
Number(supplyInfo.startTokenId) -
currentOffset,
),
);
if (result.length < expectedResultLength) {
// fresh contracts might be delayed in indexing, so we fallback to RPC
return getNFTsFromRPC(options);
}
return result;
}
@@ -121,6 +142,27 @@ async function getNFTsFromInsight(
async function getNFTsFromRPC(
options: BaseTransactionOptions<GetNFTsParams>,
): Promise<NFT[]> {
const { startTokenId, maxSupply } = await getSupplyInfo(options);
const start = BigInt(options.start ?? 0) + startTokenId;
const count = BigInt(options.count ?? DEFAULT_QUERY_ALL_COUNT);
const maxId = min(maxSupply, start + count);
const promises: ReturnType<typeof getNFT>[] = [];
for (let i = start; i < maxId; i++) {
promises.push(
getNFT({
...options,
tokenId: i,
includeOwner: options.includeOwners ?? false,
useIndexer: false,
}),
);
}
return await Promise.all(promises);
}
async function getSupplyInfo(options: BaseTransactionOptions<GetNFTsParams>) {
const [startTokenId_, maxSupply] = await Promise.allSettled([
startTokenId(options),
nextTokenIdToMint(options),
@@ -145,22 +187,9 @@ async function getNFTsFromRPC(
}
return [startTokenId__, maxSupply_] as const;
});
const start = BigInt(options.start ?? 0) + startTokenId_;
const count = BigInt(options.count ?? DEFAULT_QUERY_ALL_COUNT);
const maxId = min(maxSupply + startTokenId_, start + count);
const promises: ReturnType<typeof getNFT>[] = [];
for (let i = start; i < maxId; i++) {
promises.push(
getNFT({
...options,
tokenId: i,
includeOwner: options.includeOwners ?? false,
}),
);
}
return await Promise.all(promises);
return {
startTokenId: startTokenId_,
maxSupply,
};
}
@@ -54,6 +54,7 @@ async function getOwnedNFTsFromRPC(
getNFT({
contract: options.contract,
tokenId,
useIndexer: false,
}).then((nft) => ({
...nft,
// add the owner to the NFT since we know it
@@ -57,10 +57,7 @@ export type BuyDirectListingButtonProps = Omit<
* Since it uses the TransactionButton, it can take in any props that can be passed
* to the [`TransactionButton`](https://portal.thirdweb.com/references/typescript/v5/TransactionButton)
*
*
* @param props
* @returns <TransactionButton />
*
* @param props props of type [BuyDirectListingButtonProps](https://portal.thirdweb.com/references/typescript/v5/BuyDirectListingButtonProps)
* @example
* ```tsx
* import { BuyDirectListingButton } from "thirdweb/react";
+2
View File
@@ -1,5 +1,7 @@
# @thirdweb-dev/wagmi-adapter
## 0.2.57
## 0.2.56
## 0.2.55
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/wagmi-adapter",
"version": "0.2.56",
"version": "0.2.57",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"