Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee427d590 | ||
|
|
24319f4570 | ||
|
|
f38b262cd0 | ||
|
|
5b35a6227b | ||
|
|
03e238515b | ||
|
|
72875f9de5 | ||
|
|
97ce74f307 | ||
|
|
3a43fb23a4 |
@@ -58,4 +58,7 @@ module.exports = [
|
||||
"/faucets",
|
||||
// -- brand kit --
|
||||
"/brand-kit",
|
||||
// -- universal bridge landing pages --
|
||||
"/universal-bridge-regions/:region_slug",
|
||||
"/enterprise",
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+23
-6
@@ -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>
|
||||
);
|
||||
|
||||
+9
-7
@@ -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),
|
||||
}))}
|
||||
|
||||
+34
-25
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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
|
||||
|
||||
+1
-4
@@ -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";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.57
|
||||
|
||||
## 0.2.56
|
||||
|
||||
## 0.2.55
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user