Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0345492e99 Version Packages (#7433)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-26 03:01:11 -07:00
gregandGitHub e650434061 [SDK] Fix: Chain name label (#7442) 2025-06-25 15:47:17 -07:00
gregandGitHub 6712911950 [SDK] Chore: Remove onrampTokenAddress set to ETH (#7441) 2025-06-25 14:22:20 -07:00
orbuloandGitHub 1387337955 [SDK] Feature: update hey-api version to 0.76.0 (#7431) 2025-06-25 12:30:12 -07:00
Phillip HoandGitHub a2b737e144 fix: typo on a field in webhook event helper (#7440) 2025-06-25 23:19:02 +08:00
Phillip HoandGitHub c5f9921dc9 feat: add webhook producer to service-utils (#7439) 2025-06-25 13:35:19 +08:00
Joaquim VergesandGitHub 830125c920 Add CLAUDE.md (#7438) 2025-06-24 19:11:06 -07:00
Joaquim VergesandGitHub af89836b6c [SDK] Handle sponsored gas on TransactionWidget (#7437) 2025-06-24 19:10:47 -07:00
MananTank d85ee5fac3 Dashboard: add "Contracts" link in contract layout header (#7436)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/yNOf1svJ8o3zjO7zQouZ/05f5e0d2-cfee-4604-8567-a630e13f8232.png)

<!--

## 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 a new optional property, `currentProjectSubpath`, to enhance the `TeamHeader` components by allowing the display of additional project navigation details, including a label, href, and icon.

### Detailed summary
- Added `currentProjectSubpath` property to `TeamHeaderLoggedIn` and `TeamHeaderDesktopUI`.
- Updated `TeamHeaderUI` to render project subpath details if provided.
- Introduced a new story in `TeamHeaderUI.stories.tsx` to showcase the `currentProjectSubpath` feature.
- Integrated `currentProjectSubpath` into the `ContractLayout` component for enhanced navigation.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for displaying an active project subpage link with an icon in the team header, enhancing breadcrumb navigation for projects.
  - Updated the team header to visually indicate the current subpage within a project.

- **Documentation**
  - Added a new Storybook variant to demonstrate the team header with an active subpage link.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-24 23:37:56 +00:00
MananTank 8bb4d4bc11 Dashboard: remove date selector from UB FTUX, improved skeletons (#7435)
<!--

## 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 focuses on refactoring the `Page` component in the `loading.tsx` file and the `PayAnalytics` component in the `PayAnalytics.tsx` file, enhancing the structure and user experience by modifying the rendering of analytics and loading states.

### Detailed summary
- Removed `ResponsiveSuspense` and `PayAnalyticsFilter` from `loading.tsx`.
- Integrated `PayAnalytics` directly into the `Page` component.
- Added `ResponsiveSuspense` with a skeleton loader in `PayAnalytics`.
- Reorganized analytics display in `PayAnalytics`, including filtering and layout adjustments.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Refactor**
  - Improved the loading experience in the PayAnalytics section with responsive skeleton placeholders during data loading.
  - Updated the layout to display analytics filters above the loading state for better usability.

- **Chores**
  - Removed unused loading and suspense logic from related pages to streamline the user interface.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-24 22:27:52 +00:00
MananTank 3c6668b6c8 [TOOL-4833] Add Manage Contract button in new public contract pages (#7434)
<!--

## 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 focuses on enhancing the handling of `projectSlug` and `teamSlug` properties across various components in the application, improving navigation and data management related to teams and projects.

### Detailed summary
- Added `projectSlug` and `teamSlug` props in several components including `cards.tsx`, `DeployedContractsPage.tsx`, and `layout.tsx`.
- Updated links to include `teamSlug` and `projectSlug` for better navigation.
- Introduced new API function `getContractImportedProjects` to fetch projects with imported contracts.
- Created `TeamSelectorCard` and `ProjectAndTeamSelectorCard` components for selecting teams and projects.
- Improved UI elements such as buttons and tooltips in `ContractHeaderUI` and `ImportModal`.
- Enhanced type definitions for better type safety and clarity in components.

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

<!-- end pr-codex -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a team and project selector UI, enabling users to easily select teams or projects from visual cards.
  - Added a project and team selector for associating contracts, allowing users to view, select, or import contracts into projects across teams.
  - Enhanced contract management by providing direct navigation to contract pages based on project selection.
  - Improved contract view links to include team and project information in URLs for clearer navigation.
  - Added functionality to fetch projects that have imported specific contracts within teams.

- **UI Improvements**
  - Updated button styles with rounded edges for a polished look.
  - Added copy address and manage contract buttons with icons and tooltips for enhanced usability.

- **Storybook/Documentation**
  - Added Storybook stories to visually test and demonstrate the new team and project selector components under various scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-06-24 21:58:48 +00:00
gregandGitHub 6aa2765a34 Feature: UB Widget Tracking and Docs (#7426) 2025-06-24 13:19:01 -07:00
122 changed files with 7376 additions and 1204 deletions
+290
View File
@@ -0,0 +1,290 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
### Package Management
- Use `pnpm install` (or `pnpm install --ignore-scripts` on Windows)
- Monorepo managed with Turborepo and pnpm workspaces
### Building
```bash
# Build all packages
pnpm build
# Build specific packages with dependencies
turbo run build --filter=./packages/*
# Development with watch mode
pnpm dev # Core thirdweb package
pnpm dashboard # Run dashboard + dependencies
pnpm playground # Run playground + dependencies
pnpm portal # Run portal docs + dependencies
```
### Testing
```bash
# Run all tests
pnpm test
# Interactive testing (thirdweb package)
cd packages/thirdweb && pnpm test:devr
# Test specific file
pnpm test:dev <path-to-test-file>
# E2E testing (dashboard)
cd apps/dashboard && pnpm playwright
```
### Code Quality
```bash
# Lint all packages
pnpm lint
# Auto-fix linting issues
pnpm fix
# Format code
turbo run format
# Biome is the primary linter/formatter
```
### Development Workflow
```bash
# Start development server for dashboard
pnpm dashboard
# Start playground for SDK testing
pnpm playground
# Generate changeset for releases
pnpm changeset
# Version packages
pnpm version-packages
```
## Repository Architecture
### Monorepo Structure
This is a Turborepo monorepo with the main thirdweb v5 SDK consolidated into `/packages/thirdweb/`. Legacy packages are in `/legacy_packages/`.
### Core Package (`/packages/thirdweb/`)
**Main Modules:**
- `client/` - ThirdwebClient foundation
- `chains/` - 50+ supported blockchain definitions
- `contract/` - Contract interaction with automatic ABI resolution
- `transaction/` - Transaction management and execution
- `wallets/` - Comprehensive wallet integration system
- `extensions/` - Modular contract extensions (ERC20, ERC721, etc.)
- `auth/` - SIWE authentication and signature verification
- `pay/` - Fiat and crypto payment infrastructure
- `storage/` - IPFS integration for decentralized storage
- `rpc/` - Low-level blockchain communication
**Exports Structure:**
The SDK uses modular exports from `src/exports/` including:
- `thirdweb.ts` - Core client and utilities
- `chains.ts` - Chain definitions
- `wallets.ts` - Wallet connectors
- `react.ts` - React hooks and components
- `extensions/` - Contract standards and protocols
### Applications (`/apps/`)
- **dashboard** - Web-based developer console (Next.js, Chakra UI)
- **playground-web** - Interactive SDK testing environment
- **portal** - Documentation site with MDX
- **nebula** - Account abstraction and smart wallet management
- **wallet-ui** - Wallet interface and testing
### Key Packages (`/packages/`)
- **thirdweb** - Main SDK (TypeScript, React, React Native)
- **engine** - thirdweb Engine API client
- **insight** - Analytics and data APIs
- **nebula** - Account abstraction client
- **service-utils** - Shared utilities across services
## Development Practices
### GitHub Workflow & Pull Requests
- **PR titles**: Must start with affected workspace in brackets (e.g. `[SDK]`, `[Dashboard]`, `[Portal]`, `[Playground]`)
- **PR descriptions**: Begin with one-sentence summary, add checklist of changes, reference issues with `Fixes #123`
- **Commits**: Keep small and topical – one logical change per commit
- **Branch naming**: Use `area/brief-topic` format (e.g. `sdk/fix-gas-estimate`). Avoid personal names
- **Reviews**: Request at least one core maintainer review. Do not self-merge unless sole package owner
- **CI requirements**: All checks (type-check, Biome, tests) must pass before merging
### Code Quality & Formatting
- **Biome**: Primary linter/formatter (rules in `biome.json`)
- **Pre-commit**: Run `pnpm biome check --apply` before committing
- **Build verification**: Run `pnpm build` after each file change to ensure everything builds
- Avoid editor-specific configs; rely on shared settings
### TypeScript Guidelines
- **Style**: Write idiomatic TypeScript with explicit function declarations and return types
- **File structure**: Limit each file to one stateless, single-responsibility function for clarity
- **Types**: Re-use shared types from `@/types` or local `types.ts` barrels
- **Interfaces vs Types**: Prefer type aliases over interface except for nominal shapes
- **Type safety**: Avoid `any` and `unknown` unless unavoidable; narrow generics when possible
- **Architecture**: Choose composition over inheritance; leverage utility types (`Partial`, `Pick`, etc.)
### Testing Strategy
- **Co-location**: Place tests alongside code: `foo.ts` ↔ `foo.test.ts`
- **Test approach**: Use real function invocations with stub data; avoid brittle mocks
- **Network mocking**: Use Mock Service Worker (MSW) for fetch/HTTP call interception
- **Test quality**: Keep tests deterministic and side-effect free
- **Running tests**: `cd packages/thirdweb && pnpm test:dev <filename>`
- **Test accounts**: Predefined accounts in `test/src/test-wallets.ts`
- **Chain forking**: Use `FORKED_ETHEREUM_CHAIN` for mainnet interactions, `ANVIL_CHAIN` for isolated tests
### SDK Development (`packages/thirdweb`)
#### Public API Guidelines
- **Exports**: Export everything via `exports/` directory, grouped by feature
- **Documentation**: Every public symbol must have comprehensive TSDoc with:
- At least one `@example` block that compiles
- Custom annotation tags (`@beta`, `@internal`, `@experimental`)
- **Comments**: Comment only ambiguous logic; avoid restating TypeScript in prose
#### Performance Optimization
- **Lazy loading**: Load heavy dependencies inside async paths to keep initial bundle lean:
```typescript
const { jsPDF } = await import("jspdf");
```
- **Bundle budgets**: Track via `package.json#size-limit`
- **Dependencies**: De-duplicate across packages through pnpm workspace hoisting
### Dashboard & Playground Development
#### UI Component Standards
- **Core components**: Import primitives from `@/components/ui/*` (Button, Input, Select, Tabs, Card, Sidebar, Badge, Separator)
- **Navigation**: Use `NavLink` for internal navigation with automatic active states
- **Organization**: Group feature-specific components under `feature/components/*` with barrel `index.ts`
#### Styling Conventions
- **CSS framework**: Tailwind CSS only – no inline styles or CSS modules
- **Class merging**: Use `cn()` from `@/lib/utils` for conditional logic
- **Design tokens**: Use design system tokens (backgrounds: `bg-card`, borders: `border-border`, muted text: `text-muted-foreground`)
- **Component API**: Expose `className` prop on root element for overrides
#### Component Architecture
- **Server Components** (Node edge):
- Start files with `import "server-only";`
- Read cookies/headers with `next/headers`
- Access server-only environment variables
- Perform heavy data fetching
- Implement redirect logic with `redirect()` from `next/navigation`
- **Client Components** (browser):
- Begin files with `'use client';`
- Handle interactive UI with React hooks (`useState`, `useEffect`, React Query, wallet hooks)
- Access browser APIs (`localStorage`, `window`, `IntersectionObserver`)
- Support fast transitions with prefetched data
#### Data Fetching Patterns
- **Server Side**:
- Always call `getAuthToken()` to retrieve JWT from cookies
- Use `Authorization: Bearer` header – never embed tokens in URLs
- Return typed results (`Project[]`, `User[]`) – avoid `any`
- **Client Side**:
- Wrap calls in React Query (`@tanstack/react-query`)
- Use descriptive, stable `queryKeys` for cache hits
- Configure `staleTime`/`cacheTime` based on freshness (default ≥ 60s)
- Keep tokens secret via internal API routes or server actions
#### Analytics Event Guidelines
- **When to add**: Only create events that answer clear product/business questions
- **Check duplicates**: Review `src/@/analytics/report.ts` first
- **Naming**:
- Event name: human-readable `<subject> <verb>` (e.g. "contract deployed")
- Function: `report<Subject><Verb>` (PascalCase)
- **Template**:
```typescript
/**
* ### Why do we need to report this event?
* - Tracks number of contracts deployed
*
* ### Who is responsible for this event?
* @username
*/
export function reportContractDeployed(properties: {
address: string;
chainId: number;
}) {
posthog.capture("contract deployed", properties);
}
```
- **Client-side only**: Never import `posthog-js` in server components
- **Changes**: Inform **#eng-core-services** before renaming/removing events
### Extension Development
- Extensions follow modular pattern in `src/extensions/`
- Auto-generated contracts from ABI definitions
- Composable functions with TypeScript safety
- Support for read/write operations
### Wallet Architecture
- Unified `Wallet` and `Account` interfaces
- Support for in-app wallets (social/email login)
- Smart wallets with account abstraction
- EIP-1193, EIP-5792, EIP-7702 standard support
## Contribution Workflow
1. **Fork and Clone**: Create fork, clone, create feature branch
2. **Install**: `pnpm install` (use `--ignore-scripts` on Windows)
3. **Develop**: Use appropriate dev commands above
4. **Test**: Write unit tests, run linting, test on demo apps
5. **Changeset**: Run `pnpm changeset` for semantic versioning
6. **PR**: Submit pull request to main branch
### Release Testing
Comment `/release-pr` on PR to trigger dev release to npm for testing.
### Changeset Guidelines
Each change in `packages/*` should contain a changeset for the appropriate package with the appropriate version bump:
- **patch**: Changes that don't impact the public API
- **minor**: Any new/modified public API
- **major**: Breaking changes (surface prominently in PR descriptions)
### Documentation Standards
- For new UI components, add Storybook stories (`*.stories.tsx`) alongside the code
- Surface breaking changes prominently in PR descriptions
- Keep documentation focused on developer experience and practical usage
## Multi-Platform Support
The SDK supports:
- **Web**: React hooks and components
- **React Native**: Mobile-specific exports and components
- **Node.js**: Server-side functionality
- **Framework Adapters**: Wagmi, Ethers compatibility layers
Key files:
- `src/exports/react.native.ts` - React Native specific exports
- `packages/react-native-adapter/` - Mobile platform shims
- `packages/wagmi-adapter/` - Wagmi ecosystem integration
@@ -1,4 +1,5 @@
import "server-only";
import { getAddress } from "thirdweb";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
export type ProjectContract = {
@@ -43,3 +44,43 @@ export async function getProjectContracts(options: {
return data.result;
}
export type PartialProject = {
id: string;
name: string;
slug: string;
image: string | null;
};
/**
* get a list of projects within a team that have a given contract imported
*/
export async function getContractImportedProjects(options: {
teamId: string;
authToken: string;
chainId: number;
contractAddress: string;
}) {
const url = new URL(
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/projects/contracts/lookup?chainId=${options.chainId}&contractAddress=${getAddress(options.contractAddress)}`,
);
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${options.authToken}`,
},
});
if (!res.ok) {
const errorMessage = await res.text();
console.error("Error fetching: /projects/contracts/lookup");
console.error(errorMessage);
return [];
}
const data = (await res.json()) as {
result: PartialProject[];
};
return data.result;
}
@@ -37,6 +37,8 @@ type ImportModalProps = {
onClose: () => void;
teamId: string;
projectId: string;
projectSlug: string;
teamSlug: string;
client: ThirdwebClient;
type: "contract" | "asset";
onSuccess?: () => void;
@@ -69,7 +71,9 @@ export const ImportModal: React.FC<ImportModalProps> = (props) => {
client={props.client}
onSuccess={props.onSuccess}
projectId={props.projectId}
projectSlug={props.projectSlug}
teamId={props.teamId}
teamSlug={props.teamSlug}
type={props.type}
/>
</DialogContent>
@@ -96,6 +100,8 @@ const importFormSchema = z.object({
function ImportForm(props: {
teamId: string;
projectId: string;
teamSlug: string;
projectSlug: string;
client: ThirdwebClient;
type: "contract" | "asset";
onSuccess?: () => void;
@@ -216,7 +222,7 @@ function ImportForm(props: {
addContractToProject.data?.result ? (
<Button asChild className="gap-2">
<Link
href={`/${chainSlug}/${addContractToProject.data.result.contractAddress}`}
href={`/team/${props.teamSlug}/${props.projectSlug}/contract/${chainSlug}/${addContractToProject.data.result.contractAddress}`}
rel="noopener noreferrer"
target="_blank"
>
@@ -0,0 +1,20 @@
"use client";
import { defineChain, type ThirdwebClient } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { BuyWidget } from "thirdweb/react";
export function BuyFundsSection(props: {
chain: ChainMetadata;
client: ThirdwebClient;
}) {
return (
<section className="flex flex-col gap-4 items-center justify-center">
<BuyWidget
amount="0"
// eslint-disable-next-line no-restricted-syntax
chain={defineChain(props.chain.chainId)}
client={props.client}
/>
</section>
);
}
@@ -1,49 +0,0 @@
"use client";
import { useTheme } from "next-themes";
import type { ThirdwebClient } from "thirdweb";
import { PayEmbed } from "thirdweb/react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { defineDashboardChain } from "@/lib/defineDashboardChain";
import { getSDKTheme } from "../../../../../../../../@/utils/sdk-component-theme";
export function PayModalButton(props: {
chainId: number;
label: string;
client: ThirdwebClient;
}) {
const { theme } = useTheme();
return (
<Dialog>
<DialogTrigger asChild>
<Button className="w-full" variant="primary">
{props.label}
</Button>
</DialogTrigger>
<DialogContent
className="border-none bg-transparent p-0 md:max-w-[360px]"
dialogCloseClassName="focus:ring-0"
dialogOverlayClassName="backdrop-blur-lg"
>
<PayEmbed
className="!w-auto"
client={props.client}
payOptions={{
prefillBuy: {
allowEdits: {
amount: true,
chain: false,
token: true,
},
// Do not include local chain overrides for chain pages
// eslint-disable-next-line no-restricted-syntax
chain: defineDashboardChain(props.chainId, undefined),
},
}}
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
/>
</DialogContent>
</Dialog>
);
}
@@ -1,92 +0,0 @@
import { useId } from "react";
export function CreditCardIcon(props: { bg: string; className?: string }) {
const linearGradientId = useId();
const linearGradientId2 = useId();
const linearGradientId3 = useId();
return (
<svg
aria-hidden="true"
className={props.className}
fill="none"
height={48}
viewBox="0 0 48 48"
width={48}
xmlns="http://www.w3.org/2000/svg"
>
<rect fill={props.bg} height={48} rx={16} width={48} />
<rect
height={46}
rx={15}
stroke={`url(#${linearGradientId})`}
strokeOpacity="0.2"
strokeWidth={2}
width={46}
x={1}
y={1}
/>
<rect fill={props.bg} height={38} rx={11} width={38} x={5} y={5} />
<rect
height={38}
rx={11}
stroke={`url(#${linearGradientId2})`}
strokeWidth={2}
width={38}
x={5}
y={5}
/>
<path
d="M32.3337 22.3333H15.667M23.167 25.6666H19.0003M15.667 20.8333L15.667 27.1666C15.667 28.1 15.667 28.5668 15.8486 28.9233C16.0084 29.2369 16.2634 29.4918 16.577 29.6516C16.9335 29.8333 17.4002 29.8333 18.3337 29.8333L29.667 29.8333C30.6004 29.8333 31.0671 29.8333 31.4236 29.6516C31.7372 29.4918 31.9922 29.2369 32.152 28.9233C32.3337 28.5668 32.3337 28.1 32.3337 27.1666V20.8333C32.3337 19.8999 32.3337 19.4332 32.152 19.0766C31.9922 18.763 31.7372 18.5081 31.4236 18.3483C31.0671 18.1666 30.6004 18.1666 29.667 18.1666L18.3337 18.1666C17.4002 18.1666 16.9335 18.1666 16.577 18.3483C16.2634 18.5081 16.0084 18.763 15.8486 19.0766C15.667 19.4332 15.667 19.8999 15.667 20.8333Z"
stroke={`url(#${linearGradientId3})`}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
/>
<defs>
<linearGradient
gradientUnits="userSpaceOnUse"
id={linearGradientId}
x1="-3.26045"
x2="45.7807"
y1="15.4021"
y2="50.1547"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset={1} stopColor="#5204BF" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
id={linearGradientId2}
x1="1.28296"
x2="42.1506"
y1="16.835"
y2="45.7956"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset={1} stopColor="#5204BF" />
</linearGradient>
<linearGradient
gradientUnits="userSpaceOnUse"
id={linearGradientId3}
x1="14.5349"
x2="27.1676"
y1="21.9102"
y2="34.6989"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset={1} stopColor="#5204BF" />
</linearGradient>
</defs>
</svg>
);
}
@@ -1,68 +0,0 @@
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import type { ThirdwebClient } from "thirdweb";
import type { ChainMetadata } from "thirdweb/chains";
import { ChainIcon } from "../../../../components/server/chain-icon";
import { PayModalButton } from "../client/PayModal";
import { CreditCardIcon } from "../icons/CreditCardIcon";
import { SectionTitle } from "./SectionTitle";
export function BuyFundsSection(props: {
chain: ChainMetadata;
client: ThirdwebClient;
}) {
const sanitizedChainName = props.chain.name.replace("Mainnet", "").trim();
return (
<section>
<SectionTitle title="Bridge" />
<div className="flex justify-center rounded-lg border border-border bg-card px-4 py-10">
<div className="flex max-w-[520px] flex-col items-center ">
<div className="flex items-center">
<ChainIcon
className="-mr-2 size-12 rounded-full border p-1"
iconUrl={props.chain.icon?.url}
/>
<CreditCardIcon
bg="hsl(var(--background))"
className="-ml-2 size-12"
/>
</div>
<div className="h-6" />
<h2 className="px-4 text-center font-semibold text-lg tracking-tight">
Bridge Funds to {sanitizedChainName}
</h2>
<div className="h-2" />
<p className="max-w-[520px] px-4 text-center text-muted-foreground text-sm">
Get {props.chain.nativeCurrency.symbol} on {sanitizedChainName} with
fiat or any token on another chain.
</p>
<div className="h-8" />
<PayModalButton
chainId={props.chain.chainId}
client={props.client}
label={`Bridge to ${sanitizedChainName}`}
/>
<div className="h-4" />
<Link
className="inline-flex items-center gap-1.5 text-muted-foreground text-sm hover:text-foreground"
href="https://portal.thirdweb.com/connect/pay/overview"
rel="noopener noreferrer"
target="_blank"
>
Learn more about Universal Bridge
<ExternalLinkIcon className="size-3" />
</Link>
</div>
</div>
</section>
);
}
@@ -2,8 +2,8 @@ import { CircleAlertIcon } from "lucide-react";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { getRawAccount } from "../../../../account/settings/getAccount";
import { getChain, getChainMetadata } from "../../utils";
import { BuyFundsSection } from "./components/client/BuyFundsSection";
import NextSteps from "./components/client/NextSteps";
import { BuyFundsSection } from "./components/server/BuyFundsSection";
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
import { ClaimChainSection } from "./components/server/ClaimChainSection";
import { ChainCTA } from "./components/server/cta-card";
@@ -83,7 +83,7 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
// if user is on a project page
if (projectMeta) {
return (
<Button asChild variant="default">
<Button asChild className="rounded-full" variant="default">
<Link
className="gap-2"
href={`/${contractInfo.chainSlug}/${contractAddress}`}
@@ -1,4 +1,4 @@
import { ExternalLinkIcon, GlobeIcon } from "lucide-react";
import { ExternalLinkIcon, GlobeIcon, Settings2Icon } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
@@ -111,6 +111,13 @@ export function ContractHeaderUI(props: {
)}
</Link>
<CopyAddressButton
address={props.clientContract.address}
className="rounded-full bg-card w-[30px] h-[30px] p-0 [&>span]:hidden [&>svg]:text-foreground"
copyIconPosition="left"
variant="outline"
/>
{socialUrls
.toSorted((a, b) => {
const aIcon = platformToIcons[a.name.toLowerCase()];
@@ -140,12 +147,29 @@ export function ContractHeaderUI(props: {
{/* bottom row */}
<div className="flex flex-row flex-wrap items-center gap-2">
<CopyAddressButton
address={props.clientContract.address}
className="rounded-full bg-card px-2.5 py-1.5 text-xs"
copyIconPosition="left"
variant="outline"
/>
<ToolTipLabel
contentClassName="max-w-[300px]"
label={
<>
View this contract in thirdweb dashboard to view contract
management interface
</>
}
>
<Button
asChild
className="rounded-full bg-card gap-1.5 text-xs py-1.5 px-2.5 h-auto"
size="sm"
variant="outline"
>
<Link
href={`/team/~/~/contract/${props.chainMetadata.slug}/${props.clientContract.address}`}
>
<Settings2Icon className="size-3.5 text-muted-foreground" />
Manage Contract
</Link>
</Button>
</ToolTipLabel>
{explorersToShow?.map((validBlockExplorer) => (
<BadgeLink
@@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
export function DeployedContractsPageHeader(props: {
teamId: string;
projectId: string;
projectSlug: string;
teamSlug: string;
client: ThirdwebClient;
}) {
const [importModalOpen, setImportModalOpen] = useState(false);
@@ -23,7 +25,9 @@ export function DeployedContractsPageHeader(props: {
setImportModalOpen(false);
}}
projectId={props.projectId}
projectSlug={props.projectSlug}
teamId={props.teamId}
teamSlug={props.teamSlug}
type="contract"
/>
@@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
export function DeployViaCLIOrImportCard(props: {
teamId: string;
projectId: string;
projectSlug: string;
teamSlug: string;
client: ThirdwebClient;
}) {
const [importModalOpen, setImportModalOpen] = useState(false);
@@ -23,7 +25,9 @@ export function DeployViaCLIOrImportCard(props: {
setImportModalOpen(false);
}}
projectId={props.projectId}
projectSlug={props.projectSlug}
teamId={props.teamId}
teamSlug={props.teamSlug}
type="contract"
/>
@@ -23,7 +23,9 @@ export function DeployedContractsPage(props: {
<DeployViaCLIOrImportCard
client={props.client}
projectId={props.projectId}
projectSlug={props.projectSlug}
teamId={props.teamId}
teamSlug={props.teamSlug}
/>
</div>
);
@@ -43,7 +43,9 @@ export default async function Layout(props: {
<DeployedContractsPageHeader
client={client}
projectId={project.id}
projectSlug={params.project_slug}
teamId={team.id}
teamSlug={params.team_slug}
/>
<TabPathLinks
links={[
@@ -33,7 +33,9 @@ export function Cards(props: {
reportAssetImportSuccessful();
}}
projectId={props.projectId}
projectSlug={props.projectSlug}
teamId={props.teamId}
teamSlug={props.teamSlug}
type="asset"
/>
@@ -1,3 +1,4 @@
import { ResponsiveSuspense } from "responsive-rsc";
import type { ThirdwebClient } from "thirdweb";
import {
getUniversalBridgeUsage,
@@ -5,7 +6,9 @@ import {
} from "@/api/analytics";
import type { Range } from "@/components/analytics/date-range-selector";
import { CodeServer } from "@/components/ui/code/code.server";
import { Skeleton } from "@/components/ui/skeleton";
import { apiCode, embedCode, sdkCode } from "./code-examples";
import { PayAnalyticsFilter } from "./PayAnalyticsFilter";
import { PayCustomersTable } from "./PayCustomersTable";
import { PayEmbedFTUX } from "./PayEmbedFTUX";
import { PaymentHistory } from "./PaymentHistory";
@@ -96,44 +99,70 @@ export async function PayAnalytics(props: {
}
return (
<div className="flex flex-col gap-10 lg:gap-6">
<GridWithSeparator>
<div className="flex items-center border-border border-b pb-6 xl:border-none xl:pb-0">
<TotalVolumePieChart
data={volumeData?.filter((x) => x.status === "completed") || []}
/>
</div>
<TotalPayVolume
data={volumeData?.filter((x) => x.status === "completed") || []}
dateFormat={dateFormat}
/>
</GridWithSeparator>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
<CardContainer>
<Payouts
data={volumeData?.filter((x) => x.status === "completed") || []}
dateFormat={dateFormat}
/>
</CardContainer>
<CardContainer>
<PaymentsSuccessRate data={volumeData || []} />
</CardContainer>
<div>
<div className="mb-4 flex justify-start">
<PayAnalyticsFilter />
</div>
<ResponsiveSuspense
fallback={
<div className="flex flex-col gap-6">
<Skeleton className="h-[350px] border rounded-xl" />
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
<Skeleton className="h-[350px] border rounded-xl" />
<Skeleton className="h-[350px] border rounded-xl" />
</div>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
<Skeleton className="h-[350px] border rounded-xl" />
<Skeleton className="h-[350px] border rounded-xl" />
</div>
<Skeleton className="h-[500px] border rounded-xl" />
</div>
}
searchParamsUsed={["from", "to", "interval"]}
>
<div className="flex flex-col gap-10 lg:gap-6">
<GridWithSeparator>
<div className="flex items-center border-border border-b pb-6 xl:border-none xl:pb-0">
<TotalVolumePieChart
data={volumeData?.filter((x) => x.status === "completed") || []}
/>
</div>
<TotalPayVolume
data={volumeData?.filter((x) => x.status === "completed") || []}
dateFormat={dateFormat}
/>
</GridWithSeparator>
<GridWithSeparator>
<div className="border-border border-b pb-6 xl:border-none xl:pb-0">
<PayNewCustomers data={walletData || []} dateFormat={dateFormat} />
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
<CardContainer>
<Payouts
data={volumeData?.filter((x) => x.status === "completed") || []}
dateFormat={dateFormat}
/>
</CardContainer>
<CardContainer>
<PaymentsSuccessRate data={volumeData || []} />
</CardContainer>
</div>
<GridWithSeparator>
<div className="border-border border-b pb-6 xl:border-none xl:pb-0">
<PayNewCustomers
data={walletData || []}
dateFormat={dateFormat}
/>
</div>
<PayCustomersTable client={props.client} data={walletData || []} />
</GridWithSeparator>
<CardContainer>
<PaymentHistory
client={props.client}
projectClientId={props.projectClientId}
teamId={props.teamId}
/>
</CardContainer>
</div>
<PayCustomersTable client={props.client} data={walletData || []} />
</GridWithSeparator>
<CardContainer>
<PaymentHistory
client={props.client}
projectClientId={props.projectClientId}
teamId={props.teamId}
/>
</CardContainer>
</ResponsiveSuspense>
</div>
);
}
@@ -1,3 +0,0 @@
"use client";
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
@@ -1,16 +1,11 @@
import { loginRedirect } from "@app/login/loginRedirect";
import { ArrowUpRightIcon } from "lucide-react";
import { redirect } from "next/navigation";
import {
ResponsiveSearchParamsProvider,
ResponsiveSuspense,
} from "responsive-rsc";
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/projects";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { PayAnalytics } from "./components/PayAnalytics";
import { PayAnalyticsFilter } from "./components/PayAnalyticsFilter";
import { getUniversalBridgeFiltersFromSearchParams } from "./components/time";
export default async function Page(props: {
@@ -54,26 +49,14 @@ export default async function Page(props: {
return (
<ResponsiveSearchParamsProvider value={searchParams}>
<div>
<div className="mb-4 flex justify-start">
<PayAnalyticsFilter />
</div>
<ResponsiveSuspense
fallback={
<div className="flex w-full items-center justify-center py-24">
<Spinner className="size-8" />
</div>
}
searchParamsUsed={["from", "to", "interval"]}
>
<PayAnalytics
client={client}
interval={interval}
projectClientId={project.publishableKey}
projectId={project.id}
range={range}
teamId={project.teamId}
/>
</ResponsiveSuspense>
<PayAnalytics
client={client}
interval={interval}
projectClientId={project.publishableKey}
projectId={project.id}
range={range}
teamId={project.teamId}
/>
<div className="h-10" />
<div className="relative overflow-hidden rounded-lg border-2 border-green-500/20 bg-gradient-to-br from-card/80 to-card/50 p-4 shadow-[inset_0_1px_2px_0_rgba(0,0,0,0.02)]">
@@ -1,13 +1,11 @@
import { redirect } from "next/navigation";
import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import { AppFooter } from "@/components/footers/app-footer";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import {
getAuthToken,
getAuthTokenWalletAddress,
} from "../../../../../../../../@/api/auth-token";
import { ErrorProvider } from "../../../../../../../../@/contexts/error-handler";
import { ErrorProvider } from "@/contexts/error-handler";
import { ContractIcon } from "@/icons/ContractIcon";
import { SharedContractLayout } from "../../../../../../(dashboard)/(chain)/[chain_id]/[contractAddress]/shared-layout";
import { getValidAccount } from "../../../../../../account/settings/getAccount";
import { TeamHeaderLoggedIn } from "../../../../../components/TeamHeader/team-header-logged-in.client";
@@ -68,6 +66,11 @@ export default async function ContractLayout(props: {
accountAddress={accountAddress}
client={client}
currentProject={project}
currentProjectSubpath={{
href: `/team/${params.team_slug}/${params.project_slug}/contracts`,
icon: <ContractIcon />,
label: "Contracts",
}}
currentTeam={team}
teamsAndProjects={teamsAndProjects}
/>
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { BoxIcon } from "lucide-react";
import { ThirdwebProvider } from "thirdweb/react";
import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
@@ -119,6 +120,19 @@ function Variants(props: { type: "mobile" | "desktop" }) {
type={props.type}
/>
</BadgeContainer>
<BadgeContainer label="Pro Plan - active subpage">
<Variant
currentProject={proTeam.projects[0]}
currentProjectSubpath={{
href: "/team/project/foo",
icon: <BoxIcon />,
label: "Foo",
}}
team={proTeam.team}
type={props.type}
/>
</BadgeContainer>
</div>
</ThirdwebProvider>
);
@@ -132,6 +146,11 @@ function Variant(props: {
team: Team;
type: "mobile" | "desktop";
currentProject?: Project;
currentProjectSubpath?: {
label: string;
href: string;
icon: React.ReactNode;
};
}) {
const Comp =
props.type === "mobile" ? TeamHeaderMobileUI : TeamHeaderDesktopUI;
@@ -149,6 +168,7 @@ function Variant(props: {
createProject={() => {}}
createTeam={() => {}}
currentProject={undefined}
currentProjectSubpath={props.currentProjectSubpath}
currentTeam={props.team}
logout={() => {}}
teamsAndProjects={teamsAndProjectsStub}
@@ -21,6 +21,13 @@ export type TeamHeaderCompProps = {
currentTeam: Team;
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
currentProject: Project | undefined;
currentProjectSubpath:
| {
label: string;
icon: React.ReactNode;
href: string;
}
| undefined;
className?: string;
account: Pick<Account, "email" | "id">;
logout: () => void;
@@ -107,6 +114,23 @@ export function TeamHeaderDesktopUI(props: TeamHeaderCompProps) {
teamsAndProjects={props.teamsAndProjects}
/>
</div>
{props.currentProjectSubpath && (
<>
<SlashSeparator />
<div className="flex items-center gap-1.5">
<div className="flex items-center justify-center rounded-full border size-6 [&>svg]:size-[50%] [&>svg]:text-muted-foreground">
{props.currentProjectSubpath.icon}
</div>
<Link
className="flex flex-row items-center gap-2 font-semibold text-sm"
href={props.currentProjectSubpath.href}
>
{props.currentProjectSubpath.label}
</Link>
</div>
</>
)}
</>
)}
</div>
@@ -28,6 +28,11 @@ export function TeamHeaderLoggedIn(props: {
account: Pick<Account, "email" | "id">;
accountAddress: string;
client: ThirdwebClient;
currentProjectSubpath?: {
label: string;
href: string;
icon: React.ReactNode;
};
}) {
// identify the account
useIdentifyAccount({
@@ -89,6 +94,7 @@ export function TeamHeaderLoggedIn(props: {
);
},
currentProject: props.currentProject,
currentProjectSubpath: props.currentProjectSubpath,
currentTeam: props.currentTeam,
logout: logout,
teamsAndProjects: props.teamsAndProjects,
@@ -0,0 +1,84 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { teamStub } from "@/storybook/stubs";
import { storybookThirdwebClient } from "@/storybook/utils";
import { TeamSelectorCard } from "./team-selector";
const meta: Meta<typeof TeamSelectorCard> = {
component: TeamSelectorCard,
decorators: [
(Story) => (
<div className="py-20 flex justify-center items-center">
<Story />
</div>
),
],
parameters: {
nextjs: {
appDirectory: true,
},
},
title: "selectors/TeamSelectorCard",
};
export default meta;
type Story = StoryObj<typeof TeamSelectorCard>;
export const TwoTeams: Story = {
args: {
client: storybookThirdwebClient,
paths: undefined,
searchParams: "",
teams: [teamStub("1", "free"), teamStub("2", "starter")],
},
};
export const FiveTeams: Story = {
args: {
client: storybookThirdwebClient,
paths: undefined,
searchParams: "",
teams: [
teamStub("1", "free"),
teamStub("2", "starter"),
teamStub("3", "growth"),
teamStub("4", "pro"),
teamStub("5", "scale"),
],
},
};
export const WithSearchParams: Story = {
args: {
client: storybookThirdwebClient,
paths: undefined,
searchParams: "tab=overview&section=analytics",
teams: [
teamStub("1", "free"),
teamStub("2", "starter"),
teamStub("3", "growth"),
],
},
};
export const WithPaths: Story = {
args: {
client: storybookThirdwebClient,
paths: ["projects", "123", "settings"],
searchParams: "",
teams: [teamStub("1", "free"), teamStub("2", "starter")],
},
};
export const WithPathsAndSearchParams: Story = {
args: {
client: storybookThirdwebClient,
paths: ["projects", "123", "settings"],
searchParams: "tab=overview&section=analytics",
teams: [
teamStub("1", "free"),
teamStub("2", "starter"),
teamStub("3", "growth"),
teamStub("4", "pro"),
],
},
};
@@ -0,0 +1,73 @@
import { ChevronRightIcon, UsersIcon } from "lucide-react";
import Link from "next/link";
import type { ThirdwebClient } from "thirdweb";
import type { Team } from "@/api/team";
import { GradientAvatar } from "@/components/blocks/avatar/gradient-avatar";
import { TeamPlanBadge } from "@/components/blocks/TeamPlanBadge";
export function createTeamLink(params: {
team: Team;
paths: string[] | undefined;
searchParams: string | undefined;
}) {
const pathsSegment = params.paths?.length ? `/${params.paths.join("/")}` : "";
const searchParamsSegment = params.searchParams
? `?${params.searchParams}`
: "";
return `/team/${params.team.slug}${pathsSegment}${searchParamsSegment}`;
}
export function TeamSelectorCard(props: {
teams: Team[];
client: ThirdwebClient;
searchParams: string;
paths: string[] | undefined;
}) {
return (
<div className="w-full max-w-lg rounded-xl border bg-card shadow-2xl">
<div className="flex flex-col border-b p-4 lg:p-6">
<div className="mb-2 self-start rounded-full border p-2">
<UsersIcon className="size-5 text-muted-foreground" />
</div>
<h1 className="mb-0.5 font-semibold text-xl tracking-tight">
Select a team
</h1>
<p className="text-muted-foreground text-sm">
You are currently a member of multiple teams
<br />
Select a team to view this page
</p>
</div>
<div className="flex flex-col [&>*:not(:last-child)]:border-b">
{props.teams.map((team) => {
return (
<div
className="group relative flex items-center gap-3 px-4 py-4 hover:bg-accent/50 lg:px-6"
key={team.id}
>
<GradientAvatar
className="size-8 rounded-full border"
client={props.client}
id={team.id}
src={team.image || ""}
/>
<Link
className="before:absolute before:inset-0"
href={createTeamLink({
paths: props.paths,
searchParams: props.searchParams,
team,
})}
>
{team.name}
</Link>
<TeamPlanBadge plan={team.billingPlan} teamSlug={team.slug} />
<ChevronRightIcon className="ml-auto size-4 text-muted-foreground group-hover:text-foreground" />
</div>
);
})}
</div>
</div>
);
}
@@ -1,14 +1,11 @@
import { ChevronRightIcon, UsersIcon } from "lucide-react";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getTeams, type Team } from "@/api/team";
import { GradientAvatar } from "@/components/blocks/avatar/gradient-avatar";
import { TeamPlanBadge } from "@/components/blocks/TeamPlanBadge";
import { getTeams } from "@/api/team";
import { AppFooter } from "@/components/footers/app-footer";
import { DotsBackgroundPattern } from "@/components/ui/background-patterns";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { TeamHeader } from "../../components/TeamHeader/team-header";
import { createTeamLink, TeamSelectorCard } from "./components/team-selector";
export default async function Page(props: {
params: Promise<{
@@ -69,54 +66,12 @@ export default async function Page(props: {
<div className="relative flex grow flex-col overflow-hidden">
<DotsBackgroundPattern />
<div className="container z-10 flex grow flex-col items-center justify-center py-20">
<div className="w-full max-w-lg rounded-xl border bg-card shadow-2xl">
<div className="flex flex-col border-b p-4 lg:p-6">
<div className="mb-2 self-start rounded-full border p-2">
<UsersIcon className="size-5 text-muted-foreground" />
</div>
<h1 className="mb-0.5 font-semibold text-xl tracking-tight">
Select a team
</h1>
<p className="text-muted-foreground text-sm">
You are currently a member of multiple teams
<br />
Select a team to view this page
</p>
</div>
<div className="flex flex-col [&>*:not(:last-child)]:border-b">
{teams.map((team) => {
return (
<div
className="group relative flex items-center gap-3 px-4 py-4 hover:bg-accent/50 lg:px-6"
key={team.id}
>
<GradientAvatar
className="size-8 rounded-full border"
client={client}
id={team.id}
src={team.image || ""}
/>
<Link
className="before:absolute before:inset-0"
href={createTeamLink({
paths: params.paths,
searchParams: searchParamsString,
team,
})}
>
{team.name}
</Link>
<TeamPlanBadge
plan={team.billingPlan}
teamSlug={team.slug}
/>
<ChevronRightIcon className="ml-auto size-4 text-muted-foreground group-hover:text-foreground" />
</div>
);
})}
</div>
</div>
<TeamSelectorCard
client={client}
paths={params.paths}
searchParams={searchParamsString}
teams={teams}
/>
</div>
</div>
@@ -124,11 +79,3 @@ export default async function Page(props: {
</div>
);
}
function createTeamLink(params: {
team: Team;
paths: string[] | undefined;
searchParams: string | undefined;
}) {
return `/team/${params.team.slug}/${(params.paths || [])?.join("/") || ""}${params.searchParams ? `?${params.searchParams}` : ""}`;
}
@@ -0,0 +1,133 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import type { PartialProject } from "@/api/getProjectContracts";
import { projectStub, teamStub } from "@/storybook/stubs";
import { storybookLog, storybookThirdwebClient } from "@/storybook/utils";
import { ProjectAndTeamSelectorCard } from "./TeamAndProjectSelectorCard";
const meta = {
component: ProjectAndTeamSelectorCard,
decorators: [
(Story) => (
<div className="py-20 flex justify-center items-center">
<Story />
</div>
),
],
parameters: {
nextjs: {
appDirectory: true,
},
},
title: "selectors/TeamAndProjectSelectorCard",
} satisfies Meta<typeof ProjectAndTeamSelectorCard>;
export default meta;
type Story = StoryObj<typeof meta>;
// Helper function to create PartialProject from full Project
function createPartialProject(
project: ReturnType<typeof projectStub>,
): PartialProject {
return {
id: project.id,
image: project.image,
name: project.name,
slug: project.slug,
};
}
export const OneTeamFiveProjects: Story = {
args: {
client: storybookThirdwebClient,
description: "Select a project to continue",
onSelect: (selected) => {
console.log("Selected:", selected);
},
teamAndProjects: [
{
projects: [
createPartialProject(projectStub("1", "team-1")),
createPartialProject(projectStub("2", "team-1")),
createPartialProject(projectStub("3", "team-1")),
createPartialProject(projectStub("4", "team-1")),
createPartialProject(projectStub("5", "team-1")),
],
team: teamStub("1", "growth"),
},
],
},
};
export const TwoTeamsTwoProjectsEach: Story = {
args: {
client: storybookThirdwebClient,
description: "Select a project to continue",
onSelect: (selected) => {
storybookLog(selected);
},
teamAndProjects: [
{
projects: [
createPartialProject(projectStub("1", "team-1")),
createPartialProject(projectStub("2", "team-1")),
],
team: teamStub("1", "free"),
},
{
projects: [
createPartialProject(projectStub("3", "team-2")),
createPartialProject(projectStub("4", "team-2")),
],
team: teamStub("2", "starter"),
},
],
},
};
export const TwoTeamsOneWithThreeProjectsOtherWithZero: Story = {
args: {
client: storybookThirdwebClient,
description: "Select a project to continue",
onSelect: (selected) => {
storybookLog(selected);
},
teamAndProjects: [
{
projects: [
createPartialProject(projectStub("1", "team-1")),
createPartialProject(projectStub("2", "team-1")),
createPartialProject(projectStub("3", "team-1")),
],
team: teamStub("1", "growth"),
},
{
projects: [],
team: teamStub("2", "free"), // Empty projects array
},
],
},
};
export const TwoTeamsLotsOfProjects: Story = {
args: {
client: storybookThirdwebClient,
description: "Select a project to continue",
onSelect: (selected) => {
console.log("Selected:", selected);
},
teamAndProjects: [
{
projects: Array.from({ length: 100 }, (_, i) =>
createPartialProject(projectStub(`${i + 1}`, "team-1")),
),
team: teamStub("1", "growth"),
},
{
projects: Array.from({ length: 100 }, (_, i) =>
createPartialProject(projectStub(`${i + 1}`, "team-2")),
),
team: teamStub("2", "growth"),
},
],
},
};
@@ -0,0 +1,109 @@
"use client";
import { BoxIcon, ChevronRightIcon } from "lucide-react";
import type { ThirdwebClient } from "thirdweb";
import type { PartialProject } from "@/api/getProjectContracts";
import type { Team } from "@/api/team";
import { GradientAvatar } from "@/components/blocks/avatar/gradient-avatar";
import { ProjectAvatar } from "@/components/blocks/avatar/project-avatar";
import { TeamPlanBadge } from "@/components/blocks/TeamPlanBadge";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function ProjectAndTeamSelectorCard(props: {
teamAndProjects: {
team: Team;
projects: PartialProject[];
}[];
client: ThirdwebClient;
description: React.ReactNode;
onSelect: (selected: { team: Team; project: PartialProject }) => void;
}) {
const teamAndProjects = props.teamAndProjects.filter(
({ projects }) => projects.length > 0,
);
const showTeamHeader = teamAndProjects.length > 1;
return (
<div className="w-full max-w-lg rounded-xl border bg-card shadow-2xl">
<div className="flex flex-col border-b p-4 lg:p-6">
<div className="mb-2 self-start rounded-full border p-2">
<BoxIcon className="size-5 text-muted-foreground" />
</div>
<h1 className="mb-0.5 font-semibold text-xl tracking-tight">
Select a project
</h1>
<p className="text-muted-foreground text-sm">{props.description}</p>
</div>
<div
className={cn("flex flex-col", showTeamHeader && "p-4 lg:p-6 gap-6")}
>
{teamAndProjects.map(({ team, projects }) => {
// If multiple teams, show team name and then projects
return (
<div key={team.id}>
<div
className={cn(
showTeamHeader &&
"border rounded-xl overflow-hidden bg-background",
)}
>
{showTeamHeader && (
<div className={cn("px-4 py-3 border-b bg-muted/50")}>
<div className="flex items-center gap-2">
<GradientAvatar
className="size-6 rounded-full border"
client={props.client}
id={team.id}
src={team.image || ""}
/>
<span className="font-medium text-sm">{team.name}</span>
<TeamPlanBadge
plan={team.billingPlan}
teamSlug={team.slug}
/>
</div>
</div>
)}
<div className="max-h-[320px] overflow-y-auto">
<div className="[&>*:not(:last-child)]:border-b">
{projects.map((project) => (
<Button
className={cn(
"h-auto group relative flex items-center gap-3 px-4 py-4 hover:bg-accent/50 lg:px-6 w-full rounded-none justify-start",
showTeamHeader && "lg:pl-5",
)}
key={project.slug}
onClick={() =>
props.onSelect({
project,
team,
})
}
variant="ghost"
>
<ProjectAvatar
className="size-6 rounded-full border"
client={props.client}
src={project.image || ""}
/>
<span className="text-sm font-medium">
{project.name}
</span>
<ChevronRightIcon className="ml-auto size-3.5 text-muted-foreground/50 group-hover:text-foreground" />
</Button>
))}
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
);
}
@@ -0,0 +1,80 @@
"use client";
import type { ThirdwebClient } from "thirdweb";
import type { PartialProject } from "@/api/getProjectContracts";
import type { Team } from "@/api/team";
import { useAddContractToProject } from "@/hooks/project-contracts";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { ProjectAndTeamSelectorCard } from "../../../../../components/TeamAndProjectSelectorCard";
export function SelectProjectForContract(props: {
chainSlug: string;
contractAddress: string;
client: ThirdwebClient;
teamAndProjects: {
team: Team;
projects: PartialProject[];
}[];
}) {
const router = useDashboardRouter();
return (
<ProjectAndTeamSelectorCard
client={props.client}
description={
<>
This contract is imported in multiple projects
<br />
Select a project to view this contract
</>
}
onSelect={(selection) => {
router.push(
`/team/${selection.team.slug}/${selection.project.slug}/contract/${props.chainSlug}/${props.contractAddress}`,
);
}}
teamAndProjects={props.teamAndProjects}
/>
);
}
export function ImportAndSelectProjectForContract(props: {
chainSlug: string;
chainId: number;
contractAddress: string;
client: ThirdwebClient;
teamAndProjects: {
team: Team;
projects: PartialProject[];
}[];
}) {
const router = useDashboardRouter();
const addToProject = useAddContractToProject();
return (
<ProjectAndTeamSelectorCard
client={props.client}
description={
<>
This contract is not imported in any projects
<br />
Select a project to import the contract and continue
</>
}
onSelect={(selection) => {
// do not await - send request and move to the contract page
addToProject.mutate({
chainId: props.chainId.toString(),
contractAddress: props.contractAddress,
contractType: undefined,
deploymentType: undefined,
projectId: selection.project.id,
teamId: selection.team.id,
});
router.push(
`/team/${selection.team.slug}/${selection.project.slug}/contract/${props.chainSlug}/${props.contractAddress}`,
);
}}
teamAndProjects={props.teamAndProjects}
/>
);
}
@@ -0,0 +1,147 @@
import { notFound, redirect } from "next/navigation";
import { getAddress } from "thirdweb";
import { getAuthToken } from "@/api/auth-token";
import {
getContractImportedProjects,
type PartialProject,
} from "@/api/getProjectContracts";
import { getProjects } from "@/api/projects";
import { getTeams, type Team } from "@/api/team";
import { AppFooter } from "@/components/footers/app-footer";
import { DotsBackgroundPattern } from "@/components/ui/background-patterns";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { fetchChain } from "@/utils/fetchChain";
import { getValidAccount } from "../../../../../../account/settings/getAccount";
import { loginRedirect } from "../../../../../../login/loginRedirect";
import { TeamHeader } from "../../../../../components/TeamHeader/team-header";
import {
ImportAndSelectProjectForContract,
SelectProjectForContract,
} from "./components/project-selector";
export default async function Page(props: {
params: Promise<{
chain: string;
contractAddress: string;
}>;
}) {
const params = await props.params;
const pagePath = `/team/~/~/contract/${params.chain}/${params.contractAddress}`;
const contractAddress = getAddress(params.contractAddress);
const [authToken, chainMetadata, account, teams] = await Promise.all([
getAuthToken(),
fetchChain(params.chain),
await getValidAccount(pagePath),
getTeams(),
]);
if (!chainMetadata) {
notFound();
}
if (!authToken || !account || !teams) {
loginRedirect(pagePath);
}
// get the list of projects in each team where this contract is imported
// filter out teams with no projects
const teamAndProjectWithContracts = (
await Promise.all(
teams.map(async (team) => {
return {
projects: await getContractImportedProjects({
authToken,
chainId: chainMetadata.chainId,
contractAddress,
teamId: team.id,
}).catch(() => []),
team,
};
}),
)
).filter((x) => x.projects.length > 0);
const projectImports: Array<{
team: Team;
project: PartialProject;
}> = [];
for (const teamAndProject of teamAndProjectWithContracts) {
for (const project of teamAndProject.projects) {
projectImports.push({
project,
team: teamAndProject.team,
});
}
}
// if contract imported in only one project, redirect to it directly
if (projectImports.length === 1 && projectImports[0]) {
redirect(
`/team/${projectImports[0].team.slug}/${projectImports[0].project.slug}/contract/${chainMetadata.slug}/${contractAddress}`,
);
}
const client = getClientThirdwebClient({
jwt: authToken,
teamId: undefined,
});
// if contract imported in multiple projects
// user needs to select one
if (projectImports.length > 1) {
return (
<ProjectSelectionLayout>
<SelectProjectForContract
chainSlug={chainMetadata.slug}
client={client}
contractAddress={contractAddress}
teamAndProjects={teamAndProjectWithContracts}
/>
</ProjectSelectionLayout>
);
}
// if contract not imported in any projects
// user needs to select one from all projects, import the contract in the selected project and redirect
const teamAndAllProjects = await Promise.all(
teams.map(async (team) => {
return {
projects: await getProjects(team.slug).catch(() => []),
team,
};
}),
);
return (
<ProjectSelectionLayout>
<ImportAndSelectProjectForContract
chainId={chainMetadata.chainId}
chainSlug={chainMetadata.slug}
client={client}
contractAddress={contractAddress}
teamAndProjects={teamAndAllProjects}
/>
</ProjectSelectionLayout>
);
}
function ProjectSelectionLayout(props: { children: React.ReactNode }) {
return (
<div className="relative flex min-h-dvh flex-col">
<div className="border-b bg-card">
<TeamHeader />
</div>
<div className="relative flex grow flex-col overflow-hidden">
<DotsBackgroundPattern />
<div className="container z-10 flex grow flex-col items-center justify-center py-20">
{props.children}
</div>
</div>
<AppFooter className="relative z-10" />
</div>
);
}
+249 -119
View File
@@ -1603,164 +1603,287 @@ let returnType: Element;
# Buy Crypto
---
## PayEmbed
## BuyWidget
Embed a prebuilt UI for funding wallets, purchases or transactions with crypto or fiat.
Widget is a prebuilt UI for purchasing a specific token.
### Example
#### Default configuration
#### Basic usage
By default, the `PayEmbed` component will allows users to fund their wallets with crypto or fiat on any of the supported chains..
The `BuyWidget` component requires `client`, `chain`, and `amount` props to function.
```tsx
<PayEmbed client={client} />;
import { ethereum } from "thirdweb/chains";
<BuyWidget
client={client}
chain={ethereum}
amount="0.1"
/>
```
#### Top up wallets
#### Buy a specific token
You can set the `mode` option to `"fund_wallet"` to allow users to top up their wallets with crypto or fiat.
You can specify a token to purchase by passing the `tokenAddress` prop.
```tsx
<PayEmbed
<BuyWidget
client={client}
payOptions={{
mode: "fund_wallet",
metadata: {
name: "Get funds", // title of the payment modal
},
prefillBuy: {
chain: base, // chain to prefill the buy screen with
amount: "0.01", // amount to prefill the buy screen with
},
}}
/>;
```
#### Direct Payments
You can set the `mode` option to `"direct_payment"` to allow users to make a direct payment to a wallet address.
```tsx
<PayEmbed
client={client}
theme={"light"}
payOptions={{
mode: "direct_payment",
paymentInfo: {
amount: "35",
chain: base,
token: getDefaultToken(base, "USDC"),
sellerAddress: "0x...", // the wallet address of the seller
},
metadata: {
name: "Black Hoodie (Size L)",
image: "/drip-hoodie.png",
},
}}
/>;
```
#### Transactions
You can set the `mode` option to `"transaction"` to allow users to execute a transaction with a different wallet, chain or token.
```tsx
<PayEmbed
client={client}
payOptions={{
mode: "transaction",
// can be any transaction
transaction: claimTo({
contract: nftContract,
quantity: 1n,
tokenId: 0n,
to: "0x...",
}),
// this could be any metadata, including NFT metadata
metadata: {
name: "VIP Ticket",
image: "https://...",
},
}}
/>;
```
You can also handle ERC20 payments by passing `erc20value` to your transaction:
```tsx
<PayEmbed
client={client}
payOptions={{
mode: "transaction",
transaction: prepareContractCall({
contract: yourContract,
functionName: "purchase",
args: [arg1, arg2, ...],
erc20value: {
token: USDC_TOKEN_ADDRESS, // the erc20 token required to purchase
amount: toUnits("100", 6), // the amount of erc20 token required to purchase
},
}),
}}
/>
```
#### Enable/Disable payment methods
You can disable the use of crypto or fiat by setting the `buyWithCrypto` or `buyWithFiat` options to `false`.
```tsx
<PayEmbed
client={client}
payOptions={{
buyWithFiat: false,
}}
/>;
chain={ethereum}
amount="100"
tokenAddress="0xA0b86a33E6417E4df2057B2d3C6d9F7cc11b0a70"
/>
```
#### Customize the UI
You can customize the UI of the `PayEmbed` component by passing a custom theme object to the `theme` prop.
You can customize the UI of the `BuyWidget` component by passing a custom theme object to the `theme` prop.
```tsx
<PayEmbed
<BuyWidget
client={client}
chain={ethereum}
amount="0.1"
theme={darkTheme({
colors: {
modalBg: "red",
},
})}
/>;
/>
```
Refer to the [Theme](https://portal.thirdweb.com/references/typescript/v5/Theme) type for more details.
#### Configure the wallet connection
#### Update the Title
You can customize the wallet connection flow by passing a `connectOptions` object to the `PayEmbed` component.
You can update the title of the widget by passing a `title` prop to the `BuyWidget` component.
```tsx
<PayEmbed
<BuyWidget
client={client}
connectOptions={{
connectModal: {
size: "compact",
title: "Sign in",
},
}}
/>;
chain={ethereum}
amount="0.1"
title="Buy ETH"
/>
```
Refer to the [PayEmbedConnectOptions](https://portal.thirdweb.com/references/typescript/v5/PayEmbedConnectOptions) type for more details.
#### Configure the wallet connection
You can customize the wallet connection flow by passing a `connectOptions` object to the `BuyWidget` component.
```tsx
<BuyWidget
client={client}
chain={ethereum}
amount="0.1"
connectOptions={{
connectModal: {
size: 'compact',
title: "Sign in",
}
}}
/>
```
Refer to the [BuyWidgetConnectOptions](https://portal.thirdweb.com/references/typescript/v5/BuyWidgetConnectOptions) type for more details.
```ts
function PayEmbed(props: PayEmbedProps): Element;
function BuyWidget(props: BuyWidgetProps): Element;
```
### Parameters
Props of type [PayEmbedProps](https://portal.thirdweb.com/references/typescript/v5/PayEmbedProps) to configure the PayEmbed component.
Props of type [BuyWidgetProps](https://portal.thirdweb.com/references/typescript/v5/BuyWidgetProps) to configure the BuyWidget component.
#### Type
```ts
let props: {
activeWallet?: Wallet;
amount: string;
chain: Chain;
className?: string;
client: ThirdwebClient;
connectOptions?: BuyWidgetConnectOptions;
description?: string;
hiddenWallets?: Array<WalletId>;
image?: string;
locale?: LocaleId;
onCancel?: () => void;
onError?: (error: Error) => void;
onSuccess?: () => void;
paymentLinkId?: string;
presetOptions?: [number, number, number];
purchaseData?: Record<string, unknown>;
style?: React.CSSProperties;
supportedTokens?: SupportedTokens;
theme?: "light" | "dark" | Theme;
title?: string;
tokenAddress?: Address;
};
```
### Returns
```ts
let returnType: Element;
```
## CheckoutWidget
Widget is a prebuilt UI for making direct payments to a wallet address.
### Example
#### Basic usage
The `CheckoutWidget` component requires `client`, `chain`, `amount`, `tokenAddress`, and `sellerAddress` props to function.
```tsx
import { base } from "thirdweb/chains";
import { getDefaultToken } from "thirdweb/utils";
<CheckoutWidget
client={client}
chain={base}
amount="35"
tokenAddress={getDefaultToken(base, "USDC")}
sellerAddress="0x..."
/>
```
#### With metadata
You can add metadata to display product information.
```tsx
<CheckoutWidget
client={client}
chain={base}
amount="35"
tokenAddress={getDefaultToken(base, "USDC")}
sellerAddress="0x..."
title="Black Hoodie (Size L)"
description="Premium cotton hoodie"
image="/drip-hoodie.png"
/>
```
```ts
function CheckoutWidget(props: CheckoutWidgetProps): Element;
```
### Parameters
Props of type [CheckoutWidgetProps](https://portal.thirdweb.com/references/typescript/v5/CheckoutWidgetProps) to configure the CheckoutWidget component.
#### Type
```ts
let props: {
activeWallet?: Wallet;
amount: string;
chain: Chain;
className?: string;
client: ThirdwebClient;
connectOptions?: CheckoutWidgetConnectOptions;
description?: string;
hiddenWallets?: Array<WalletId>;
image?: string;
locale?: LocaleId;
onCancel?: () => void;
onError?: (error: Error) => void;
onSuccess?: () => void;
paymentLinkId?: string;
purchaseData?: Record<string, unknown>;
sellerAddress: Address;
style?: React.CSSProperties;
supportedTokens?: SupportedTokens;
theme?: "light" | "dark" | Theme;
title?: string;
tokenAddress: Address;
};
```
### Returns
```ts
let returnType: Element;
```
## TransactionWidget
Widget is a prebuilt UI for executing a transaction with a different wallet, chain or token.
### Example
#### Basic usage
The `TransactionWidget` component requires `client` and `transaction` props to function.
```tsx
import { claimTo } from "thirdweb/extensions/erc1155";
<TransactionWidget
client={client}
transaction={claimTo({
contract: nftContract,
quantity: 1n,
tokenId: 0n,
to: "0x...",
})}
/>
```
#### With metadata
You can add metadata to display transaction information.
```tsx
<TransactionWidget
client={client}
transaction={claimTo({
contract: nftContract,
quantity: 1n,
tokenId: 0n,
to: "0x...",
})}
title="VIP Ticket"
description="Exclusive NFT collection"
image="https://..."
/>
```
#### ERC20 payments
You can handle ERC20 payments by passing `erc20value` to your transaction:
```tsx
import { prepareContractCall } from "thirdweb";
import { toUnits } from "thirdweb/utils";
<TransactionWidget
client={client}
transaction={prepareContractCall({
contract: yourContract,
functionName: "purchase",
args: [arg1, arg2, ...],
erc20value: {
token: USDC_TOKEN_ADDRESS, // the erc20 token required to purchase
amount: toUnits("100", 6), // the amount of erc20 token required to purchase
},
})}
/>
```
```ts
function TransactionWidget(props: TransactionWidgetProps): Element;
```
### Parameters
Props of type [TransactionWidgetProps](https://portal.thirdweb.com/references/typescript/v5/TransactionWidgetProps) to configure the TransactionWidget component.
#### Type
@@ -1769,14 +1892,21 @@ let props: {
activeWallet?: Wallet;
className?: string;
client: ThirdwebClient;
connectOptions?: PayEmbedConnectOptions;
connectOptions?: TransactionWidgetConnectOptions;
description?: string;
hiddenWallets?: Array<WalletId>;
image?: string;
locale?: LocaleId;
onCancel?: () => void;
onError?: (error: Error) => void;
onSuccess?: () => void;
paymentLinkId?: string;
payOptions?: PayUIOptions;
purchaseData?: Record<string, unknown>;
style?: React.CSSProperties;
supportedTokens?: SupportedTokens;
theme?: "light" | "dark" | Theme;
title?: string;
transaction: PreparedTransaction;
};
```
@@ -1,154 +0,0 @@
import { createMetadata, DocImage } from "@doc";
export const metadata = createMetadata({
image: {
title: "thirdweb Universal Bridge - Customize PayEmbed",
icon: "thirdweb",
},
title: "thirdweb Universal Bridge - Customize PayEmbed - thirdweb",
description:
"thirdweb Universal Bridge - Customizing embeddable onramps and crypto purchase flows",
});
# PayEmbed Customization
Learn how to customize the `PayEmbed`. You can find a selection of popular customizations below. For the full list of props, you can [view the full reference](/references/typescript/v5/PayEmbedProps).
---
## Customize Supported Tokens
You can enable users to select your token on a given chain by passing an array of `SupportedTokens`. Note that this array overrides all default tokens listed for that chain.
```tsx
<PayEmbed
client={client}
payOptions={{
supportedTokens: {
"1": [
{
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
name: "USD Coin",
symbol: "USDC",
icon: usdcIcon,
},
],
},
}}
/>
```
---
## Prefill Destination Token
In the case you want your users to purchase your token by default in your application, you can choose to pre-fill the Pay purchase flow with `prefillBuy` .
For example, if you wanted users to only purchase Ethereum on Base network, you could do as follows:
```tsx
import { base } from "thirdweb/chains";
<PayEmbed
client={client}
payOptions={{
prefillBuy: {
token: {
address: "0x866a087038f7C12cf33EF91aC5b1AcE6Ac1DA788",
name: "Base ETH",
symbol: "ETH",
icon: "...", // optional
},
chain: base,
allowEdits: {
amount: true, // allow editing buy amount
token: false, // disable selecting buy token
chain: false, // disable selecting buy chain
},
},
}}
/>;
```
If you'd like to prefill a purchase with a native token, you can set the chain without passing a token:
```tsx
<PayEmbed
client={client}
payOptions={{
prefillBuy: {
chain: base,
},
}}
/>
```
---
## Preferred Provider
You can specify which onramp provider to present to your users. By default, we choose a recommended provider based on the location of the user, KYC status, and currency. Please make sure your set provider is [available in your country](../onramp-providers).
```tsx
<PayEmbed
client={client}
payOptions={{
preferredProvider: "COINBASE" | "STRIPE" | "TRANSAK",
}}
/>
```
---
## Disable Payment Methods
In some cases, you may only want to show users fiat or crypto payment options for your onchain goods or services. You can do this by setting either `buyWithCrypto` or `buyWithFiat` to `false`.
#### Disable Buy With Crypto
```tsx
<PayEmbed
client={client}
payOptions={{
buyWithCrypto: false,
}}
/>
```
#### Disable Buy With Fiat
```tsx
<PayEmbed
client={client}
payOptions={{
buyWithFiat: false,
}}
/>
```
---
## Theme
You can set the theme for the component, which is set to `"dark"` by default. theme can be set to either `"dark"` , `"light"` or a custom theme object.
We have [`lightTheme`](/references/typescript/v5/lightTheme) or [`darkTheme`](/references/typescript/v5/darkTheme) providers that you can override to kickstart customization.
You can refer to our [`Theme`](/references/typescript/v5/Theme) page for a full view of customizable options if you’d prefer to create a custom theme from scratch.
#### Provided Themes
```tsx
<PayEmbed theme='light' />
<PayEmbed theme='dark' />
```
#### Custom Theme
```tsx
import { darkTheme } from 'thirdweb/react';
// Using custom theme
<PayEmbed theme={darkTheme({ ... })} />
```
@@ -84,7 +84,7 @@ const onClick = () => {
### Converting a wagmi wallet client to a thirdweb wallet
You can use the thirdweb SDK within a wagmi application by setting the wagmi connected account as the thirdweb 'active wallet'. After that, you can use all of the react components and hooks normally, including `PayEmbed`, `TransactionButton`, etc.
You can use the thirdweb SDK within a wagmi application by setting the wagmi connected account as the thirdweb 'active wallet'. After that, you can use all of the react components and hooks normally, including `BuyWidget`, `TransactionButton`, etc.
```ts
// Assumes you've wrapped your application in a `<ThirdwebProvider>`
@@ -126,7 +126,7 @@ You can view a fully functioning wagmi + thirdweb app in this [github repository
## Privy
Similarly, you can use the thirdweb SDK with privy by setting the privy wallet as the thirdweb 'active wallet'. After that, you can use all of the react components, functions and hooks normally, including `PayEmbed`, `TransactionButton`, etc.
Similarly, you can use the thirdweb SDK with privy by setting the privy wallet as the thirdweb 'active wallet'. After that, you can use all of the react components, functions and hooks normally, including `BuyWidget`, `TransactionButton`, etc.
```ts
// Assumes you've wrapped your application in a `<ThirdwebProvider>`
@@ -28,39 +28,33 @@ Try out the demo for yourself in the [buy crypto live playground](https://playgr
## Usage with UI Components
The easiest way to buy crypto is to use the `PayEmbed` prebuilt UI component. Note that the `ConnectButton` also has a built-in funding flow that can also be customised.
The easiest way to buy crypto is to use the `BuyWidget` prebuilt UI component.
```tsx
import { PayEmbed } from "thirdweb/react";
import { BuyWidget } from "thirdweb/react";
import { base } from "thirdweb/chains";
function App() {
return (
<PayEmbed
<BuyWidget
client={client}
payOptions={{
mode: "fund_wallet",
metadata: {
name: "Get funds",
},
prefillBuy: {
chain: base,
amount: "0.01",
},
// ... theme, currency, amounts, payment methods, etc.
}}
chain={base}
amount="0.01"
title="Get funds"
// ... theme, onSuccess, onError, etc.
/>
);
}
```
Check out the [PayEmbed API reference](/references/typescript/v5/PayEmbed) for more information.
Check out the [BuyWidget API reference](/references/typescript/v5/BuyWidget) for more information.
<Stack>
<ArticleIconCard
title="PayEmbed"
title="BuyWidget"
icon={ReactIcon}
href="/references/typescript/v5/PayEmbed"
href="/references/typescript/v5/BuyWidget"
description="Prebuilt UI to buy crypto by swapping or fiat onramps"
/>
@@ -26,55 +26,32 @@ Try out the demo for yourself in the [live playground](https://playground.thirdw
/>
</Stack>
## Usage with PayEmbed
You can use the `PayEmbed` component to let users execute a transaction from any chain, paying with any token.
```tsx
import { claimTo } from "thirdweb/extensions/erc1155";
import { PayEmbed, useActiveAccount } from "thirdweb/react";
function App() {
const account = useActiveAccount();
const { data: nft } = useReadContract(getNFT, {
contract: nftContract,
tokenId: 0n,
});
return (
<PayEmbed
client={client}
payOptions={{
mode: "transaction",
transaction: claimTo({
contract: nftContract,
quantity: 1n,
tokenId: 0n,
to: account?.address,
}),
metadata: nft?.metadata,
}}
/>
);
}
```
Check out the [PayEmbed API reference](/references/typescript/v5/PayEmbed) for more information.
<Stack>
<ArticleIconCard
title="PayEmbed"
icon={ReactIcon}
href="/references/typescript/v5/PayEmbed"
description="Prebuilt UI to buy crypto by swapping or fiat onramps"
/>
</Stack>
## Usage with regular transactions
Any transaction sent with `useSendTransaction` or with `TransactionButton` will prompt the user to choose a different payment method if they don't have enough balance for the transaction.
You can turn off this behaviour by setting the `payModal` option to `false`.
You can turn off this behaviour by setting the `payModal` option to `false`.
## Usage with TransactionWidget
The `TransactionWidget` component provides a complete UI for executing transactions with built-in cross-chain payment support.
```tsx
import { TransactionWidget } from "thirdweb/react";
import { prepareContractCall } from "thirdweb";
function MyComponent() {
return (
<TransactionWidget
client={client}
transaction={prepareContractCall({
contract: myContract,
method: "function mint(uint256 quantity)",
params: [1n],
})}
amount="0.1"
title="Mint NFT"
/>
);
}
```
@@ -13,7 +13,6 @@ const tagsToGroup = {
"@appURI": "App URI",
"@auth": "Auth",
"@bridge": "Universal Bridge",
"@buyCrypto": "Buy Crypto",
"@chain": "Chain",
"@claimConditions": "Claim Conditions",
"@client": "Client",
@@ -70,7 +69,6 @@ const sidebarGroupOrder: TagKey[] = [
"@social",
"@auth",
"@nft",
"@buyCrypto",
"@nftDrop",
"@claimConditions",
"@delayedReveal",
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/engine
## 3.1.0
### Minor Changes
- [#7431](https://github.com/thirdweb-dev/js/pull/7431) [`1387337`](https://github.com/thirdweb-dev/js/commit/1387337955a38f1527266b0a6146bb18d86426aa) Thanks [@PaoloRollo](https://github.com/PaoloRollo)! - update hey-api version to 0.76.0
## 3.0.4
### Patch Changes
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/engine",
"version": "3.0.4",
"version": "3.1.0",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
@@ -27,12 +27,9 @@
"dist/*",
"src/*"
],
"dependencies": {
"@hey-api/client-fetch": "0.10.0"
},
"devDependencies": {
"@biomejs/biome": "2.0.4",
"@hey-api/openapi-ts": "0.72.1",
"@hey-api/openapi-ts": "0.76.0",
"rimraf": "6.0.1",
"tslib": "^2.8.1"
},
+1 -1
View File
@@ -5,7 +5,7 @@ import {
createClient,
createConfig,
type ClientOptions as DefaultClientOptions,
} from "@hey-api/client-fetch";
} from "./client/index.js";
import type { ClientOptions } from "./types.gen.js";
/**
+189
View File
@@ -0,0 +1,189 @@
import type { Client, Config, RequestOptions } from "./types.js";
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from "./utils.js";
type ReqInit = Omit<RequestInit, "body" | "headers"> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};
const interceptors = createInterceptors<
Request,
Response,
unknown,
RequestOptions
>();
const request: Client["request"] = async (options) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}
if (opts.body && opts.bodySerializer) {
opts.body = opts.bodySerializer(opts.body);
}
// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.body === "") {
opts.headers.delete("Content-Type");
}
const url = buildUrl(opts);
const requestInit: ReqInit = {
redirect: "follow",
...opts,
};
let request = new Request(url, requestInit);
for (const fn of interceptors.request._fns) {
if (fn) {
request = await fn(request, opts);
}
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response = await _fetch(request);
for (const fn of interceptors.response._fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
const result = {
request,
response,
};
if (response.ok) {
if (
response.status === 204 ||
response.headers.get("Content-Length") === "0"
) {
return opts.responseStyle === "data"
? {}
: {
data: {},
...result,
};
}
const parseAs =
(opts.parseAs === "auto"
? getParseAs(response.headers.get("Content-Type"))
: opts.parseAs) ?? "json";
let data: any;
switch (parseAs) {
case "arrayBuffer":
case "blob":
case "formData":
case "json":
case "text":
data = await response[parseAs]();
break;
case "stream":
return opts.responseStyle === "data"
? response.body
: {
data: response.body,
...result,
};
}
if (parseAs === "json") {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === "data"
? data
: {
data,
...result,
};
}
let error = await response.text();
try {
error = JSON.parse(error);
} catch {
// noop
}
let finalError = error;
for (const fn of interceptors.error._fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError;
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === "data"
? undefined
: {
error: finalError,
...result,
};
};
return {
buildUrl,
connect: (options) => request({ ...options, method: "CONNECT" }),
delete: (options) => request({ ...options, method: "DELETE" }),
get: (options) => request({ ...options, method: "GET" }),
getConfig,
head: (options) => request({ ...options, method: "HEAD" }),
interceptors,
options: (options) => request({ ...options, method: "OPTIONS" }),
patch: (options) => request({ ...options, method: "PATCH" }),
post: (options) => request({ ...options, method: "POST" }),
put: (options) => request({ ...options, method: "PUT" }),
request,
setConfig,
trace: (options) => request({ ...options, method: "TRACE" }),
};
};
@@ -0,0 +1,22 @@
export type { Auth } from "../core/auth.js";
export type { QuerySerializerOptions } from "../core/bodySerializer.js";
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from "../core/bodySerializer.js";
export { buildClientParams } from "../core/params.js";
export { createClient } from "./client.js";
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
OptionsLegacyParser,
RequestOptions,
RequestResult,
ResponseStyle,
TDataShape,
} from "./types.js";
export { createConfig, mergeHeaders } from "./utils.js";
+222
View File
@@ -0,0 +1,222 @@
import type { Auth } from "../core/auth.js";
import type {
Client as CoreClient,
Config as CoreConfig,
} from "../core/types.js";
import type { Middleware } from "./utils.js";
export type ResponseStyle = "data" | "fields";
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, "body" | "headers" | "method">,
CoreConfig {
/**
* Base URL for all requests made by this client.
*/
baseUrl?: T["baseUrl"];
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: (request: Request) => ReturnType<typeof fetch>;
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
*
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/
next?: never;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
* You can override this behavior with any of the {@link Body} methods.
* Select `stream` if you don't want to parse response data at all.
*
* @default 'auto'
*/
parseAs?:
| "arrayBuffer"
| "auto"
| "blob"
| "formData"
| "json"
| "stream"
| "text";
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
* @default 'fields'
*/
responseStyle?: ResponseStyle;
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T["throwOnError"];
}
export interface RequestOptions<
TResponseStyle extends ResponseStyle = "fields",
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends Config<{
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
}> {
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
}
export type RequestResult<
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
> = ThrowOnError extends true
? Promise<
TResponseStyle extends "data"
? TData extends Record<string, unknown>
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends "data"
?
| (TData extends Record<string, unknown>
? TData[keyof TData]
: TData)
| undefined
: (
| {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
error: undefined;
}
| {
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
}
) & {
request: Request;
response: Response;
}
>;
export interface ClientOptions {
baseUrl?: string;
responseStyle?: ResponseStyle;
throwOnError?: boolean;
}
type MethodFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
>(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type RequestFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
>(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method"> &
Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = <
TData extends {
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
url: string;
},
>(
options: Pick<TData, "url"> & Options<TData>,
) => string;
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
interceptors: Middleware<Request, Response, unknown, RequestOptions>;
};
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
export interface TDataShape {
body?: unknown;
headers?: unknown;
path?: unknown;
query?: unknown;
url: string;
}
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
> = OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
"body" | "path" | "query" | "url"
> &
Omit<TData, "url">;
export type OptionsLegacyParser<
TData = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
> = TData extends { body?: any }
? TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
"body" | "headers" | "url"
> &
TData
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "url"> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "headers">
: TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
"headers" | "url"
> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "body">
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "url"> & TData;
+417
View File
@@ -0,0 +1,417 @@
import { getAuthToken } from "../core/auth.js";
import type {
QuerySerializer,
QuerySerializerOptions,
} from "../core/bodySerializer.js";
import { jsonBodySerializer } from "../core/bodySerializer.js";
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from "../core/pathSerializer.js";
import type { Client, ClientOptions, Config, RequestOptions } from "./types.js";
interface PathSerializer {
path: Record<string, unknown>;
url: string;
}
const PATH_PARAM_RE = /\{[^{}]+\}/g;
type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited";
type MatrixStyle = "label" | "matrix" | "simple";
type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = "simple";
if (name.endsWith("*")) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith(".")) {
name = name.substring(1);
style = "label";
} else if (name.startsWith(";")) {
name = name.substring(1);
style = "matrix";
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(
match,
serializeArrayParam({ explode, name, style, value }),
);
continue;
}
if (typeof value === "object") {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === "matrix") {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === "label" ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const createQuerySerializer = <T = unknown>({
allowReserved,
array,
object,
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
if (queryParams && typeof queryParams === "object") {
for (const name in queryParams) {
const value = queryParams[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved,
explode: true,
name,
style: "form",
value,
...array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === "object") {
const serializedObject = serializeObjectParam({
allowReserved,
explode: true,
name,
style: "deepObject",
value: value as Record<string, unknown>,
...object,
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join("&");
};
return querySerializer;
};
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (
contentType: string | null,
): Exclude<Config["parseAs"], "auto"> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
return "stream";
}
const cleanContent = contentType.split(";")[0]?.trim();
if (!cleanContent) {
return;
}
if (
cleanContent.startsWith("application/json") ||
cleanContent.endsWith("+json")
) {
return "json";
}
if (cleanContent === "multipart/form-data") {
return "formData";
}
if (
["application/", "audio/", "image/", "video/"].some((type) =>
cleanContent.startsWith(type),
)
) {
return "blob";
}
if (cleanContent.startsWith("text/")) {
return "text";
}
return;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, "security"> &
Pick<RequestOptions, "auth" | "query"> & {
headers: Headers;
}) => {
for (const auth of security) {
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue;
}
const name = auth.name ?? "Authorization";
switch (auth.in) {
case "query":
if (!options.query) {
options.query = {};
}
options.query[name] = token;
break;
case "cookie":
options.headers.append("Cookie", `${name}=${token}`);
break;
case "header":
default:
options.headers.set(name, token);
break;
}
return;
}
};
export const buildUrl: Client["buildUrl"] = (options) => {
const url = getUrl({
baseUrl: options.baseUrl as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === "function"
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
let url = (baseUrl ?? "") + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : "";
if (search.startsWith("?")) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseUrl?.endsWith("/")) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
export const mergeHeaders = (
...headers: Array<Required<Config>["headers"] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header || typeof header !== "object") {
continue;
}
const iterator =
header instanceof Headers ? header.entries() : Object.entries(header);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, v as string);
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
typeof value === "object" ? JSON.stringify(value) : (value as string),
);
}
}
}
return mergedHeaders;
};
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
options: Options,
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
class Interceptors<Interceptor> {
_fns: (Interceptor | null)[];
constructor() {
this._fns = [];
}
clear() {
this._fns = [];
}
getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === "number") {
return this._fns[id] ? id : -1;
} else {
return this._fns.indexOf(id);
}
}
exists(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
return !!this._fns[index];
}
eject(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = null;
}
}
update(id: number | Interceptor, fn: Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = fn;
return id;
} else {
return false;
}
}
use(fn: Interceptor) {
this._fns = [...this._fns, fn];
return this._fns.length - 1;
}
}
// `createInterceptors()` response, meant for external use as it does not
// expose internals
export interface Middleware<Req, Res, Err, Options> {
error: Pick<
Interceptors<ErrInterceptor<Err, Res, Req, Options>>,
"eject" | "use"
>;
request: Pick<Interceptors<ReqInterceptor<Req, Options>>, "eject" | "use">;
response: Pick<
Interceptors<ResInterceptor<Res, Req, Options>>,
"eject" | "use"
>;
}
// do not add `Middleware` as return type so we can use _fns internally
export const createInterceptors = <Req, Res, Err, Options>() => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
});
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: "form",
},
object: {
explode: true,
style: "deepObject",
},
});
const defaultHeaders = {
"Content-Type": "application/json",
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
parseAs: "auto",
querySerializer: defaultQuerySerializer,
...override,
});
+40
View File
@@ -0,0 +1,40 @@
export type AuthToken = string | undefined;
export interface Auth {
/**
* Which part of the request do we use to send the auth?
*
* @default 'header'
*/
in?: "header" | "query" | "cookie";
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string;
scheme?: "basic" | "bearer";
type: "apiKey" | "http";
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token =
typeof callback === "function" ? await callback(auth) : callback;
if (!token) {
return;
}
if (auth.scheme === "bearer") {
return `Bearer ${token}`;
}
if (auth.scheme === "basic") {
return `Basic ${btoa(token)}`;
}
return token;
};
@@ -0,0 +1,84 @@
import type {
ArrayStyle,
ObjectStyle,
SerializerOptions,
} from "./pathSerializer.js";
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: any) => any;
export interface QuerySerializerOptions {
allowReserved?: boolean;
array?: SerializerOptions<ArrayStyle>;
object?: SerializerOptions<ObjectStyle>;
}
const serializeFormDataPair = (data: FormData, key: string, value: unknown) => {
if (typeof value === "string" || value instanceof Blob) {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
const serializeUrlSearchParamsPair = (
data: URLSearchParams,
key: string,
value: unknown,
) => {
if (typeof value === "string") {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
) => {
const data = new FormData();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(data, key, value);
}
});
return data;
},
};
export const jsonBodySerializer = {
bodySerializer: <T>(body: T) =>
JSON.stringify(body, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
) => {
const data = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value);
}
});
return data.toString();
},
};
+141
View File
@@ -0,0 +1,141 @@
type Slot = "body" | "headers" | "path" | "query";
export type Field =
| {
in: Exclude<Slot, "body">;
key: string;
map?: string;
}
| {
in: Extract<Slot, "body">;
key?: string;
map?: string;
};
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
}
export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = {
$body_: "body",
$headers_: "headers",
$path_: "path",
$query_: "query",
};
const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map<
string,
{
in: Slot;
map?: string;
}
>;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map();
}
for (const config of fields) {
if ("in" in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
});
}
} else if (config.args) {
buildKeyMap(config.args, map);
}
}
return map;
};
interface Params {
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === "object" && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
};
export const buildClientParams = (
args: ReadonlyArray<unknown>,
fields: FieldsConfig,
) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
};
const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index];
}
if (!config) {
continue;
}
if ("in" in config) {
if (config.key) {
const field = map.get(config.key)!;
const name = field.map || config.key;
(params[field.in] as Record<string, unknown>)[name] = arg;
} else {
params.body = arg;
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key);
if (field) {
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
} else {
const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix),
);
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[
key.slice(prefix.length)
] = value;
} else {
for (const [slot, allowed] of Object.entries(
config.allowExtra ?? {},
)) {
if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
}
}
}
}
}
}
}
stripEmptySlots(params);
return params;
};
@@ -0,0 +1,179 @@
interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
name: string;
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean;
style: T;
}
export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited";
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = "label" | "matrix" | "simple";
export type ObjectStyle = "form" | "deepObject";
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string;
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case "label":
return ".";
case "matrix":
return ";";
case "simple":
return ",";
default:
return "&";
}
};
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case "form":
return ",";
case "pipeDelimited":
return "|";
case "spaceDelimited":
return "%20";
default:
return ",";
}
};
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case "label":
return ".";
case "matrix":
return ";";
case "simple":
return ",";
default:
return "&";
}
};
export const serializeArrayParam = ({
allowReserved,
explode,
name,
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[];
}) => {
if (!explode) {
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
switch (style) {
case "label":
return `.${joinedValues}`;
case "matrix":
return `;${name}=${joinedValues}`;
case "simple":
return joinedValues;
default:
return `${name}=${joinedValues}`;
}
}
const separator = separatorArrayExplode(style);
const joinedValues = value
.map((v) => {
if (style === "label" || style === "simple") {
return allowReserved ? v : encodeURIComponent(v as string);
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
});
})
.join(separator);
return style === "label" || style === "matrix"
? separator + joinedValues
: joinedValues;
};
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return "";
}
if (typeof value === "object") {
throw new Error(
"Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.",
);
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
export const serializeObjectParam = ({
allowReserved,
explode,
name,
style,
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date;
valueOnly?: boolean;
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
}
if (style !== "deepObject" && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [
...values,
key,
allowReserved ? (v as string) : encodeURIComponent(v as string),
];
});
const joinedValues = values.join(",");
switch (style) {
case "form":
return `${name}=${joinedValues}`;
case "label":
return `.${joinedValues}`;
case "matrix":
return `;${name}=${joinedValues}`;
default:
return joinedValues;
}
}
const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === "deepObject" ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator);
return style === "label" || style === "matrix"
? separator + joinedValues
: joinedValues;
};
+98
View File
@@ -0,0 +1,98 @@
import type { Auth, AuthToken } from "./auth.js";
import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from "./bodySerializer.js";
export interface Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
> {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
connect: MethodFn;
delete: MethodFn;
get: MethodFn;
getConfig: () => Config;
head: MethodFn;
options: MethodFn;
patch: MethodFn;
post: MethodFn;
put: MethodFn;
request: RequestFn;
setConfig: (config: Config) => Config;
trace: MethodFn;
}
export interface Config {
/**
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
*
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit["headers"]
| Record<
string,
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined
| unknown
>;
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?:
| "CONNECT"
| "DELETE"
| "GET"
| "HEAD"
| "OPTIONS"
| "PATCH"
| "POST"
| "PUT"
| "TRACE";
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
* style, and reserved characters are percent-encoded.
*
* This method will have no effect if the native `paramsSerializer()` Axios
* API function is used.
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
}
+31 -29
View File
@@ -4,35 +4,37 @@ import type {
Client,
Options as ClientOptions,
TDataShape,
} from "@hey-api/client-fetch";
} from "./client/index.js";
import { client as _heyApiClient } from "./client.gen.js";
import type {
CreateAccountData,
CreateAccountResponse,
CreateAccountResponses,
EncodeFunctionDataData,
EncodeFunctionDataResponse,
EncodeFunctionDataResponses,
GetNativeBalanceData,
GetNativeBalanceResponse,
GetNativeBalanceErrors,
GetNativeBalanceResponses,
GetTransactionAnalyticsData,
GetTransactionAnalyticsResponse,
GetTransactionAnalyticsResponses,
GetTransactionAnalyticsSummaryData,
GetTransactionAnalyticsSummaryResponse,
GetTransactionAnalyticsSummaryErrors,
GetTransactionAnalyticsSummaryResponses,
ListAccountsData,
ListAccountsResponse,
ListAccountsResponses,
ReadContractData,
ReadContractResponse,
ReadContractResponses,
SearchTransactionsData,
SearchTransactionsResponse,
SearchTransactionsResponses,
SendTransactionData,
SendTransactionResponse,
SendTransactionResponses,
SignMessageData,
SignMessageResponse,
SignMessageResponses,
SignTransactionData,
SignTransactionResponse,
SignTransactionResponses,
SignTypedDataData,
SignTypedDataResponse,
SignTypedDataResponses,
WriteContractData,
WriteContractResponse,
WriteContractResponses,
} from "./types.gen.js";
export type Options<
@@ -60,7 +62,7 @@ export const listAccounts = <ThrowOnError extends boolean = false>(
options?: Options<ListAccountsData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
ListAccountsResponse,
ListAccountsResponses,
unknown,
ThrowOnError
>({
@@ -83,7 +85,7 @@ export const createAccount = <ThrowOnError extends boolean = false>(
options?: Options<CreateAccountData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
CreateAccountResponse,
CreateAccountResponses,
unknown,
ThrowOnError
>({
@@ -110,7 +112,7 @@ export const writeContract = <ThrowOnError extends boolean = false>(
options?: Options<WriteContractData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
WriteContractResponse,
WriteContractResponses,
unknown,
ThrowOnError
>({
@@ -137,7 +139,7 @@ export const sendTransaction = <ThrowOnError extends boolean = false>(
options?: Options<SendTransactionData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
SendTransactionResponse,
SendTransactionResponses,
unknown,
ThrowOnError
>({
@@ -164,7 +166,7 @@ export const signTransaction = <ThrowOnError extends boolean = false>(
options?: Options<SignTransactionData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
SignTransactionResponse,
SignTransactionResponses,
unknown,
ThrowOnError
>({
@@ -191,7 +193,7 @@ export const signMessage = <ThrowOnError extends boolean = false>(
options?: Options<SignMessageData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
SignMessageResponse,
SignMessageResponses,
unknown,
ThrowOnError
>({
@@ -218,7 +220,7 @@ export const signTypedData = <ThrowOnError extends boolean = false>(
options?: Options<SignTypedDataData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
SignTypedDataResponse,
SignTypedDataResponses,
unknown,
ThrowOnError
>({
@@ -245,7 +247,7 @@ export const readContract = <ThrowOnError extends boolean = false>(
options?: Options<ReadContractData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
ReadContractResponse,
ReadContractResponses,
unknown,
ThrowOnError
>({
@@ -272,8 +274,8 @@ export const getNativeBalance = <ThrowOnError extends boolean = false>(
options?: Options<GetNativeBalanceData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
GetNativeBalanceResponse,
unknown,
GetNativeBalanceResponses,
GetNativeBalanceErrors,
ThrowOnError
>({
security: [
@@ -299,7 +301,7 @@ export const encodeFunctionData = <ThrowOnError extends boolean = false>(
options?: Options<EncodeFunctionDataData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
EncodeFunctionDataResponse,
EncodeFunctionDataResponses,
unknown,
ThrowOnError
>({
@@ -326,7 +328,7 @@ export const searchTransactions = <ThrowOnError extends boolean = false>(
options?: Options<SearchTransactionsData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
SearchTransactionsResponse,
SearchTransactionsResponses,
unknown,
ThrowOnError
>({
@@ -353,7 +355,7 @@ export const getTransactionAnalytics = <ThrowOnError extends boolean = false>(
options?: Options<GetTransactionAnalyticsData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
GetTransactionAnalyticsResponse,
GetTransactionAnalyticsResponses,
unknown,
ThrowOnError
>({
@@ -382,8 +384,8 @@ export const getTransactionAnalyticsSummary = <
options?: Options<GetTransactionAnalyticsSummaryData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
GetTransactionAnalyticsSummaryResponse,
unknown,
GetTransactionAnalyticsSummaryResponses,
GetTransactionAnalyticsSummaryErrors,
ThrowOnError
>({
security: [
+7
View File
@@ -18,6 +18,7 @@ export type TransactionsFilterNested = {
};
/**
* Auto-determine Best Execution Options
* This is the default execution option. If you do not specify an execution type, and only specify a "from" string, engine will automatically determine the most optimal options for you. If you would like to specify granular options about execution strategy choose one of the other `executionOptions` type and provide them.
*/
export type AutoExecutionOptions = {
@@ -39,6 +40,9 @@ export type AutoExecutionOptions = {
chainId: string;
};
/**
* ERC4337 Execution (Smart Account)
*/
export type AaExecutionOptions = {
type: "ERC4337";
/**
@@ -73,6 +77,7 @@ export type AaExecutionOptions = {
};
/**
* AA:zksync Execution Options
* Uses zkSync native AA for execution. This type of execution is only available on zkSync chains.
*/
export type AaZksyncExecutionOptions = {
@@ -131,6 +136,7 @@ export type ListAccountsResponses = {
* EVM address in hex format
*/
address: string;
label?: string;
/**
* The predicted smart account address for use with the default thirdweb v0.7 AccountFactory
*/
@@ -167,6 +173,7 @@ export type CreateAccountResponses = {
* EVM address in hex format
*/
address: string;
label?: string;
/**
* The predicted smart account address for use with the default thirdweb v0.7 AccountFactory
*/
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Config } from "@hey-api/client-fetch";
import type { Config } from "./client/client/index.js";
import { client } from "./client/client.gen.js";
export type EngineClientOptions = {
+2 -1
View File
@@ -20,7 +20,8 @@
"jsx": "react-jsx",
"lib": [
"ES2022", // By using ES2022 we get access to the `.cause` property on `Error` instances.
"DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped.
"DOM", // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped.
"DOM.Iterable" // Adding `DOM.Iterable` for the Headers.entries() method.
],
"module": "NodeNext",
+6
View File
@@ -1,5 +1,11 @@
# @thirdweb-dev/insight
## 1.1.0
### Minor Changes
- [#7431](https://github.com/thirdweb-dev/js/pull/7431) [`1387337`](https://github.com/thirdweb-dev/js/commit/1387337955a38f1527266b0a6146bb18d86426aa) Thanks [@PaoloRollo](https://github.com/PaoloRollo)! - update hey-api version to 0.76.0
## 1.0.2
### Patch Changes
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/insight",
"version": "1.0.2",
"version": "1.1.0",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
@@ -27,12 +27,9 @@
"dist/*",
"src/*"
],
"dependencies": {
"@hey-api/client-fetch": "0.10.0"
},
"devDependencies": {
"@biomejs/biome": "2.0.4",
"@hey-api/openapi-ts": "0.72.1",
"@hey-api/openapi-ts": "0.76.0",
"rimraf": "6.0.1",
"tslib": "^2.8.1"
},
+1 -1
View File
@@ -5,7 +5,7 @@ import {
createClient,
createConfig,
type ClientOptions as DefaultClientOptions,
} from "@hey-api/client-fetch";
} from "./client/index.js";
import type { ClientOptions } from "./types.gen.js";
/**
@@ -0,0 +1,189 @@
import type { Client, Config, RequestOptions } from "./types.js";
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from "./utils.js";
type ReqInit = Omit<RequestInit, "body" | "headers"> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};
const interceptors = createInterceptors<
Request,
Response,
unknown,
RequestOptions
>();
const request: Client["request"] = async (options) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}
if (opts.body && opts.bodySerializer) {
opts.body = opts.bodySerializer(opts.body);
}
// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.body === "") {
opts.headers.delete("Content-Type");
}
const url = buildUrl(opts);
const requestInit: ReqInit = {
redirect: "follow",
...opts,
};
let request = new Request(url, requestInit);
for (const fn of interceptors.request._fns) {
if (fn) {
request = await fn(request, opts);
}
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response = await _fetch(request);
for (const fn of interceptors.response._fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
const result = {
request,
response,
};
if (response.ok) {
if (
response.status === 204 ||
response.headers.get("Content-Length") === "0"
) {
return opts.responseStyle === "data"
? {}
: {
data: {},
...result,
};
}
const parseAs =
(opts.parseAs === "auto"
? getParseAs(response.headers.get("Content-Type"))
: opts.parseAs) ?? "json";
let data: any;
switch (parseAs) {
case "arrayBuffer":
case "blob":
case "formData":
case "json":
case "text":
data = await response[parseAs]();
break;
case "stream":
return opts.responseStyle === "data"
? response.body
: {
data: response.body,
...result,
};
}
if (parseAs === "json") {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === "data"
? data
: {
data,
...result,
};
}
let error = await response.text();
try {
error = JSON.parse(error);
} catch {
// noop
}
let finalError = error;
for (const fn of interceptors.error._fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError;
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === "data"
? undefined
: {
error: finalError,
...result,
};
};
return {
buildUrl,
connect: (options) => request({ ...options, method: "CONNECT" }),
delete: (options) => request({ ...options, method: "DELETE" }),
get: (options) => request({ ...options, method: "GET" }),
getConfig,
head: (options) => request({ ...options, method: "HEAD" }),
interceptors,
options: (options) => request({ ...options, method: "OPTIONS" }),
patch: (options) => request({ ...options, method: "PATCH" }),
post: (options) => request({ ...options, method: "POST" }),
put: (options) => request({ ...options, method: "PUT" }),
request,
setConfig,
trace: (options) => request({ ...options, method: "TRACE" }),
};
};
@@ -0,0 +1,22 @@
export type { Auth } from "../core/auth.js";
export type { QuerySerializerOptions } from "../core/bodySerializer.js";
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from "../core/bodySerializer.js";
export { buildClientParams } from "../core/params.js";
export { createClient } from "./client.js";
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
OptionsLegacyParser,
RequestOptions,
RequestResult,
ResponseStyle,
TDataShape,
} from "./types.js";
export { createConfig, mergeHeaders } from "./utils.js";
+222
View File
@@ -0,0 +1,222 @@
import type { Auth } from "../core/auth.js";
import type {
Client as CoreClient,
Config as CoreConfig,
} from "../core/types.js";
import type { Middleware } from "./utils.js";
export type ResponseStyle = "data" | "fields";
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, "body" | "headers" | "method">,
CoreConfig {
/**
* Base URL for all requests made by this client.
*/
baseUrl?: T["baseUrl"];
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: (request: Request) => ReturnType<typeof fetch>;
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
*
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/
next?: never;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
* You can override this behavior with any of the {@link Body} methods.
* Select `stream` if you don't want to parse response data at all.
*
* @default 'auto'
*/
parseAs?:
| "arrayBuffer"
| "auto"
| "blob"
| "formData"
| "json"
| "stream"
| "text";
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
* @default 'fields'
*/
responseStyle?: ResponseStyle;
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T["throwOnError"];
}
export interface RequestOptions<
TResponseStyle extends ResponseStyle = "fields",
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends Config<{
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
}> {
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
}
export type RequestResult<
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
> = ThrowOnError extends true
? Promise<
TResponseStyle extends "data"
? TData extends Record<string, unknown>
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends "data"
?
| (TData extends Record<string, unknown>
? TData[keyof TData]
: TData)
| undefined
: (
| {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
error: undefined;
}
| {
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
}
) & {
request: Request;
response: Response;
}
>;
export interface ClientOptions {
baseUrl?: string;
responseStyle?: ResponseStyle;
throwOnError?: boolean;
}
type MethodFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
>(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type RequestFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
>(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method"> &
Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = <
TData extends {
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
url: string;
},
>(
options: Pick<TData, "url"> & Options<TData>,
) => string;
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
interceptors: Middleware<Request, Response, unknown, RequestOptions>;
};
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
export interface TDataShape {
body?: unknown;
headers?: unknown;
path?: unknown;
query?: unknown;
url: string;
}
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
> = OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
"body" | "path" | "query" | "url"
> &
Omit<TData, "url">;
export type OptionsLegacyParser<
TData = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
> = TData extends { body?: any }
? TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
"body" | "headers" | "url"
> &
TData
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "url"> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "headers">
: TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
"headers" | "url"
> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "body">
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "url"> & TData;
+416
View File
@@ -0,0 +1,416 @@
import { getAuthToken } from "../core/auth.js";
import type {
QuerySerializer,
QuerySerializerOptions,
} from "../core/bodySerializer.js";
import { jsonBodySerializer } from "../core/bodySerializer.js";
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from "../core/pathSerializer.js";
import type { Client, ClientOptions, Config, RequestOptions } from "./types.js";
interface PathSerializer {
path: Record<string, unknown>;
url: string;
}
const PATH_PARAM_RE = /\{[^{}]+\}/g;
type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited";
type MatrixStyle = "label" | "matrix" | "simple";
type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = "simple";
if (name.endsWith("*")) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith(".")) {
name = name.substring(1);
style = "label";
} else if (name.startsWith(";")) {
name = name.substring(1);
style = "matrix";
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(
match,
serializeArrayParam({ explode, name, style, value }),
);
continue;
}
if (typeof value === "object") {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === "matrix") {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === "label" ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const createQuerySerializer = <T = unknown>({
allowReserved,
array,
object,
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
if (queryParams && typeof queryParams === "object") {
for (const name in queryParams) {
const value = queryParams[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved,
explode: true,
name,
style: "form",
value,
...array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === "object") {
const serializedObject = serializeObjectParam({
allowReserved,
explode: true,
name,
style: "deepObject",
value: value as Record<string, unknown>,
...object,
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join("&");
};
return querySerializer;
};
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (
contentType: string | null,
): Exclude<Config["parseAs"], "auto"> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
return "stream";
}
const cleanContent = contentType.split(";")[0]?.trim();
if (!cleanContent) {
return;
}
if (
cleanContent.startsWith("application/json") ||
cleanContent.endsWith("+json")
) {
return "json";
}
if (cleanContent === "multipart/form-data") {
return "formData";
}
if (
["application/", "audio/", "image/", "video/"].some((type) =>
cleanContent.startsWith(type),
)
) {
return "blob";
}
if (cleanContent.startsWith("text/")) {
return "text";
}
return;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, "security"> &
Pick<RequestOptions, "auth" | "query"> & {
headers: Headers;
}) => {
for (const auth of security) {
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue;
}
const name = auth.name ?? "Authorization";
switch (auth.in) {
case "query":
if (!options.query) {
options.query = {};
}
options.query[name] = token;
break;
case "cookie":
options.headers.append("Cookie", `${name}=${token}`);
break;
default:
options.headers.set(name, token);
break;
}
return;
}
};
export const buildUrl: Client["buildUrl"] = (options) => {
const url = getUrl({
baseUrl: options.baseUrl as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === "function"
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
let url = (baseUrl ?? "") + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : "";
if (search.startsWith("?")) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseUrl?.endsWith("/")) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
export const mergeHeaders = (
...headers: Array<Required<Config>["headers"] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header || typeof header !== "object") {
continue;
}
const iterator =
header instanceof Headers ? header.entries() : Object.entries(header);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, v as string);
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
typeof value === "object" ? JSON.stringify(value) : (value as string),
);
}
}
}
return mergedHeaders;
};
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
options: Options,
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
class Interceptors<Interceptor> {
_fns: (Interceptor | null)[];
constructor() {
this._fns = [];
}
clear() {
this._fns = [];
}
getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === "number") {
return this._fns[id] ? id : -1;
} else {
return this._fns.indexOf(id);
}
}
exists(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
return !!this._fns[index];
}
eject(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = null;
}
}
update(id: number | Interceptor, fn: Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = fn;
return id;
} else {
return false;
}
}
use(fn: Interceptor) {
this._fns = [...this._fns, fn];
return this._fns.length - 1;
}
}
// `createInterceptors()` response, meant for external use as it does not
// expose internals
export interface Middleware<Req, Res, Err, Options> {
error: Pick<
Interceptors<ErrInterceptor<Err, Res, Req, Options>>,
"eject" | "use"
>;
request: Pick<Interceptors<ReqInterceptor<Req, Options>>, "eject" | "use">;
response: Pick<
Interceptors<ResInterceptor<Res, Req, Options>>,
"eject" | "use"
>;
}
// do not add `Middleware` as return type so we can use _fns internally
export const createInterceptors = <Req, Res, Err, Options>() => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
});
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: "form",
},
object: {
explode: true,
style: "deepObject",
},
});
const defaultHeaders = {
"Content-Type": "application/json",
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
parseAs: "auto",
querySerializer: defaultQuerySerializer,
...override,
});
+40
View File
@@ -0,0 +1,40 @@
export type AuthToken = string | undefined;
export interface Auth {
/**
* Which part of the request do we use to send the auth?
*
* @default 'header'
*/
in?: "header" | "query" | "cookie";
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string;
scheme?: "basic" | "bearer";
type: "apiKey" | "http";
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token =
typeof callback === "function" ? await callback(auth) : callback;
if (!token) {
return;
}
if (auth.scheme === "bearer") {
return `Bearer ${token}`;
}
if (auth.scheme === "basic") {
return `Basic ${btoa(token)}`;
}
return token;
};
@@ -0,0 +1,84 @@
import type {
ArrayStyle,
ObjectStyle,
SerializerOptions,
} from "./pathSerializer.js";
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: any) => any;
export interface QuerySerializerOptions {
allowReserved?: boolean;
array?: SerializerOptions<ArrayStyle>;
object?: SerializerOptions<ObjectStyle>;
}
const serializeFormDataPair = (data: FormData, key: string, value: unknown) => {
if (typeof value === "string" || value instanceof Blob) {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
const serializeUrlSearchParamsPair = (
data: URLSearchParams,
key: string,
value: unknown,
) => {
if (typeof value === "string") {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
) => {
const data = new FormData();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(data, key, value);
}
});
return data;
},
};
export const jsonBodySerializer = {
bodySerializer: <T>(body: T) =>
JSON.stringify(body, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
) => {
const data = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value);
}
});
return data.toString();
},
};
+141
View File
@@ -0,0 +1,141 @@
type Slot = "body" | "headers" | "path" | "query";
export type Field =
| {
in: Exclude<Slot, "body">;
key: string;
map?: string;
}
| {
in: Extract<Slot, "body">;
key?: string;
map?: string;
};
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
}
export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = {
$body_: "body",
$headers_: "headers",
$path_: "path",
$query_: "query",
};
const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map<
string,
{
in: Slot;
map?: string;
}
>;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map();
}
for (const config of fields) {
if ("in" in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
});
}
} else if (config.args) {
buildKeyMap(config.args, map);
}
}
return map;
};
interface Params {
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === "object" && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
};
export const buildClientParams = (
args: ReadonlyArray<unknown>,
fields: FieldsConfig,
) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
};
const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index];
}
if (!config) {
continue;
}
if ("in" in config) {
if (config.key) {
const field = map.get(config.key)!;
const name = field.map || config.key;
(params[field.in] as Record<string, unknown>)[name] = arg;
} else {
params.body = arg;
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key);
if (field) {
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
} else {
const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix),
);
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[
key.slice(prefix.length)
] = value;
} else {
for (const [slot, allowed] of Object.entries(
config.allowExtra ?? {},
)) {
if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
}
}
}
}
}
}
}
stripEmptySlots(params);
return params;
};
@@ -0,0 +1,179 @@
interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
name: string;
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean;
style: T;
}
export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited";
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = "label" | "matrix" | "simple";
export type ObjectStyle = "form" | "deepObject";
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string;
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case "label":
return ".";
case "matrix":
return ";";
case "simple":
return ",";
default:
return "&";
}
};
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case "form":
return ",";
case "pipeDelimited":
return "|";
case "spaceDelimited":
return "%20";
default:
return ",";
}
};
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case "label":
return ".";
case "matrix":
return ";";
case "simple":
return ",";
default:
return "&";
}
};
export const serializeArrayParam = ({
allowReserved,
explode,
name,
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[];
}) => {
if (!explode) {
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
switch (style) {
case "label":
return `.${joinedValues}`;
case "matrix":
return `;${name}=${joinedValues}`;
case "simple":
return joinedValues;
default:
return `${name}=${joinedValues}`;
}
}
const separator = separatorArrayExplode(style);
const joinedValues = value
.map((v) => {
if (style === "label" || style === "simple") {
return allowReserved ? v : encodeURIComponent(v as string);
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
});
})
.join(separator);
return style === "label" || style === "matrix"
? separator + joinedValues
: joinedValues;
};
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return "";
}
if (typeof value === "object") {
throw new Error(
"Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.",
);
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
export const serializeObjectParam = ({
allowReserved,
explode,
name,
style,
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date;
valueOnly?: boolean;
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
}
if (style !== "deepObject" && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [
...values,
key,
allowReserved ? (v as string) : encodeURIComponent(v as string),
];
});
const joinedValues = values.join(",");
switch (style) {
case "form":
return `${name}=${joinedValues}`;
case "label":
return `.${joinedValues}`;
case "matrix":
return `;${name}=${joinedValues}`;
default:
return joinedValues;
}
}
const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === "deepObject" ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator);
return style === "label" || style === "matrix"
? separator + joinedValues
: joinedValues;
};
+98
View File
@@ -0,0 +1,98 @@
import type { Auth, AuthToken } from "./auth.js";
import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from "./bodySerializer.js";
export interface Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
> {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
connect: MethodFn;
delete: MethodFn;
get: MethodFn;
getConfig: () => Config;
head: MethodFn;
options: MethodFn;
patch: MethodFn;
post: MethodFn;
put: MethodFn;
request: RequestFn;
setConfig: (config: Config) => Config;
trace: MethodFn;
}
export interface Config {
/**
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
*
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit["headers"]
| Record<
string,
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined
| unknown
>;
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?:
| "CONNECT"
| "DELETE"
| "GET"
| "HEAD"
| "OPTIONS"
| "PATCH"
| "POST"
| "PUT"
| "TRACE";
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
* style, and reserved characters are percent-encoded.
*
* This method will have no effect if the native `paramsSerializer()` Axios
* API function is used.
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
}
+170 -162
View File
@@ -4,124 +4,132 @@ import type {
Client,
Options as ClientOptions,
TDataShape,
} from "@hey-api/client-fetch";
} from "./client/index.js";
import { client as _heyApiClient } from "./client.gen.js";
import type {
DeleteV1WebhooksByWebhookIdData,
DeleteV1WebhooksByWebhookIdError,
DeleteV1WebhooksByWebhookIdResponse,
DeleteV1WebhooksByWebhookIdErrors,
DeleteV1WebhooksByWebhookIdResponses,
GetV1BlocksData,
GetV1BlocksError,
GetV1BlocksResponse,
GetV1BlocksErrors,
GetV1BlocksResponses,
GetV1ContractsAbiByContractAddressData,
GetV1ContractsAbiByContractAddressError,
GetV1ContractsAbiByContractAddressResponse,
GetV1ContractsAbiByContractAddressErrors,
GetV1ContractsAbiByContractAddressResponses,
GetV1ContractsMetadataByContractAddressData,
GetV1ContractsMetadataByContractAddressError,
GetV1ContractsMetadataByContractAddressResponse,
GetV1ContractsMetadataByContractAddressErrors,
GetV1ContractsMetadataByContractAddressResponses,
GetV1EventsByContractAddressBySignatureData,
GetV1EventsByContractAddressBySignatureError,
GetV1EventsByContractAddressBySignatureResponse,
GetV1EventsByContractAddressBySignatureErrors,
GetV1EventsByContractAddressBySignatureResponses,
GetV1EventsByContractAddressData,
GetV1EventsByContractAddressError,
GetV1EventsByContractAddressResponse,
GetV1EventsByContractAddressErrors,
GetV1EventsByContractAddressResponses,
GetV1EventsData,
GetV1EventsError,
GetV1EventsResponse,
GetV1EventsErrors,
GetV1EventsResponses,
GetV1NftsBalanceByOwnerAddressData,
GetV1NftsBalanceByOwnerAddressResponse,
GetV1NftsBalanceByOwnerAddressErrors,
GetV1NftsBalanceByOwnerAddressResponses,
GetV1NftsByContractAddressByTokenIdData,
GetV1NftsByContractAddressByTokenIdError,
GetV1NftsByContractAddressByTokenIdResponse,
GetV1NftsByContractAddressByTokenIdErrors,
GetV1NftsByContractAddressByTokenIdResponses,
GetV1NftsByContractAddressData,
GetV1NftsByContractAddressError,
GetV1NftsByContractAddressResponse,
GetV1NftsByContractAddressErrors,
GetV1NftsByContractAddressResponses,
GetV1NftsCollectionsByContractAddressData,
GetV1NftsCollectionsByContractAddressError,
GetV1NftsCollectionsByContractAddressResponse,
GetV1NftsCollectionsByContractAddressErrors,
GetV1NftsCollectionsByContractAddressResponses,
GetV1NftsData,
GetV1NftsError,
GetV1NftsErrors,
GetV1NftsMetadataRefreshByContractAddressByTokenIdData,
GetV1NftsMetadataRefreshByContractAddressByTokenIdError,
GetV1NftsMetadataRefreshByContractAddressByTokenIdResponse,
GetV1NftsMetadataRefreshByContractAddressByTokenIdErrors,
GetV1NftsMetadataRefreshByContractAddressByTokenIdResponses,
GetV1NftsMetadataRefreshByContractAddressData,
GetV1NftsMetadataRefreshByContractAddressError,
GetV1NftsMetadataRefreshByContractAddressResponse,
GetV1NftsMetadataRefreshByContractAddressErrors,
GetV1NftsMetadataRefreshByContractAddressResponses,
GetV1NftsOwnersByContractAddressByTokenIdData,
GetV1NftsOwnersByContractAddressByTokenIdError,
GetV1NftsOwnersByContractAddressByTokenIdResponse,
GetV1NftsOwnersByContractAddressByTokenIdErrors,
GetV1NftsOwnersByContractAddressByTokenIdResponses,
GetV1NftsOwnersByContractAddressData,
GetV1NftsOwnersByContractAddressError,
GetV1NftsOwnersByContractAddressResponse,
GetV1NftsResponse,
GetV1NftsOwnersByContractAddressErrors,
GetV1NftsOwnersByContractAddressResponses,
GetV1NftsResponses,
GetV1NftsTransfersByContractAddressByTokenIdData,
GetV1NftsTransfersByContractAddressByTokenIdError,
GetV1NftsTransfersByContractAddressByTokenIdResponse,
GetV1NftsTransfersByContractAddressByTokenIdErrors,
GetV1NftsTransfersByContractAddressByTokenIdResponses,
GetV1NftsTransfersByContractAddressData,
GetV1NftsTransfersByContractAddressError,
GetV1NftsTransfersByContractAddressResponse,
GetV1NftsTransfersByContractAddressErrors,
GetV1NftsTransfersByContractAddressResponses,
GetV1NftsTransfersData,
GetV1NftsTransfersError,
GetV1NftsTransfersResponse,
GetV1NftsTransfersErrors,
GetV1NftsTransfersResponses,
GetV1NftsTransfersTransactionByTransactionHashData,
GetV1NftsTransfersTransactionByTransactionHashError,
GetV1NftsTransfersTransactionByTransactionHashResponse,
GetV1NftsTransfersTransactionByTransactionHashErrors,
GetV1NftsTransfersTransactionByTransactionHashResponses,
GetV1ResolveByInputData,
GetV1ResolveByInputError,
GetV1ResolveByInputResponse,
GetV1ResolveByInputErrors,
GetV1ResolveByInputResponses,
GetV1TokensData,
GetV1TokensErc20ByOwnerAddressData,
GetV1TokensErc20ByOwnerAddressResponse,
GetV1TokensErc20ByOwnerAddressErrors,
GetV1TokensErc20ByOwnerAddressResponses,
GetV1TokensErc721ByOwnerAddressData,
GetV1TokensErc721ByOwnerAddressResponse,
GetV1TokensErc721ByOwnerAddressErrors,
GetV1TokensErc721ByOwnerAddressResponses,
GetV1TokensErc1155ByOwnerAddressData,
GetV1TokensErc1155ByOwnerAddressResponse,
GetV1TokensErc1155ByOwnerAddressErrors,
GetV1TokensErc1155ByOwnerAddressResponses,
GetV1TokensErrors,
GetV1TokensLookupData,
GetV1TokensLookupResponse,
GetV1TokensLookupErrors,
GetV1TokensLookupResponses,
GetV1TokensOwnersData,
GetV1TokensOwnersError,
GetV1TokensOwnersResponse,
GetV1TokensOwnersErrors,
GetV1TokensOwnersResponses,
GetV1TokensPriceData,
GetV1TokensPriceResponse,
GetV1TokensPriceErrors,
GetV1TokensPriceResponses,
GetV1TokensPriceSupportedData,
GetV1TokensPriceSupportedResponse,
GetV1TokensResponse,
GetV1TokensPriceSupportedErrors,
GetV1TokensPriceSupportedResponses,
GetV1TokensResponses,
GetV1TokensTransfersByContractAddressData,
GetV1TokensTransfersByContractAddressError,
GetV1TokensTransfersByContractAddressResponse,
GetV1TokensTransfersByContractAddressErrors,
GetV1TokensTransfersByContractAddressResponses,
GetV1TokensTransfersData,
GetV1TokensTransfersError,
GetV1TokensTransfersResponse,
GetV1TokensTransfersErrors,
GetV1TokensTransfersResponses,
GetV1TokensTransfersTransactionByTransactionHashData,
GetV1TokensTransfersTransactionByTransactionHashError,
GetV1TokensTransfersTransactionByTransactionHashResponse,
GetV1TokensTransfersTransactionByTransactionHashErrors,
GetV1TokensTransfersTransactionByTransactionHashResponses,
GetV1TransactionsByContractAddressBySignatureData,
GetV1TransactionsByContractAddressBySignatureError,
GetV1TransactionsByContractAddressBySignatureResponse,
GetV1TransactionsByContractAddressBySignatureErrors,
GetV1TransactionsByContractAddressBySignatureResponses,
GetV1TransactionsByContractAddressData,
GetV1TransactionsByContractAddressError,
GetV1TransactionsByContractAddressResponse,
GetV1TransactionsByContractAddressErrors,
GetV1TransactionsByContractAddressResponses,
GetV1TransactionsData,
GetV1TransactionsError,
GetV1TransactionsResponse,
GetV1TransactionsErrors,
GetV1TransactionsResponses,
GetV1WalletsByWalletAddressTransactionsData,
GetV1WalletsByWalletAddressTransactionsError,
GetV1WalletsByWalletAddressTransactionsResponse,
GetV1WalletsByWalletAddressTransactionsErrors,
GetV1WalletsByWalletAddressTransactionsResponses,
GetV1WebhooksData,
GetV1WebhooksError,
GetV1WebhooksResponse,
GetV1WebhooksErrors,
GetV1WebhooksResponses,
PatchV1WebhooksByWebhookIdData,
PatchV1WebhooksByWebhookIdError,
PatchV1WebhooksByWebhookIdResponse,
PatchV1WebhooksByWebhookIdErrors,
PatchV1WebhooksByWebhookIdResponses,
PostV1DecodeByContractAddressData,
PostV1DecodeByContractAddressError,
PostV1DecodeByContractAddressResponse,
PostV1DecodeByContractAddressErrors,
PostV1DecodeByContractAddressResponses,
PostV1WebhooksData,
PostV1WebhooksError,
PostV1WebhooksResponse,
PostV1WebhooksErrors,
PostV1WebhooksResponses,
PostV1WebhooksTestData,
PostV1WebhooksTestError,
PostV1WebhooksTestResponse,
PostV1WebhooksTestErrors,
PostV1WebhooksTestResponses,
} from "./types.gen.js";
export type Options<
@@ -149,8 +157,8 @@ export const getV1Webhooks = <ThrowOnError extends boolean = false>(
options?: Options<GetV1WebhooksData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
GetV1WebhooksResponse,
GetV1WebhooksError,
GetV1WebhooksResponses,
GetV1WebhooksErrors,
ThrowOnError
>({
security: [
@@ -172,8 +180,8 @@ export const postV1Webhooks = <ThrowOnError extends boolean = false>(
options?: Options<PostV1WebhooksData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
PostV1WebhooksResponse,
PostV1WebhooksError,
PostV1WebhooksResponses,
PostV1WebhooksErrors,
ThrowOnError
>({
security: [
@@ -201,8 +209,8 @@ export const deleteV1WebhooksByWebhookId = <
options: Options<DeleteV1WebhooksByWebhookIdData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).delete<
DeleteV1WebhooksByWebhookIdResponse,
DeleteV1WebhooksByWebhookIdError,
DeleteV1WebhooksByWebhookIdResponses,
DeleteV1WebhooksByWebhookIdErrors,
ThrowOnError
>({
security: [
@@ -226,8 +234,8 @@ export const patchV1WebhooksByWebhookId = <
options: Options<PatchV1WebhooksByWebhookIdData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).patch<
PatchV1WebhooksByWebhookIdResponse,
PatchV1WebhooksByWebhookIdError,
PatchV1WebhooksByWebhookIdResponses,
PatchV1WebhooksByWebhookIdErrors,
ThrowOnError
>({
security: [
@@ -240,7 +248,7 @@ export const patchV1WebhooksByWebhookId = <
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
...options.headers,
},
});
};
@@ -253,8 +261,8 @@ export const postV1WebhooksTest = <ThrowOnError extends boolean = false>(
options?: Options<PostV1WebhooksTestData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).post<
PostV1WebhooksTestResponse,
PostV1WebhooksTestError,
PostV1WebhooksTestResponses,
PostV1WebhooksTestErrors,
ThrowOnError
>({
security: [
@@ -280,8 +288,8 @@ export const getV1Events = <ThrowOnError extends boolean = false>(
options?: Options<GetV1EventsData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
GetV1EventsResponse,
GetV1EventsError,
GetV1EventsResponses,
GetV1EventsErrors,
ThrowOnError
>({
security: [
@@ -305,8 +313,8 @@ export const getV1EventsByContractAddress = <
options: Options<GetV1EventsByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1EventsByContractAddressResponse,
GetV1EventsByContractAddressError,
GetV1EventsByContractAddressResponses,
GetV1EventsByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -330,8 +338,8 @@ export const getV1EventsByContractAddressBySignature = <
options: Options<GetV1EventsByContractAddressBySignatureData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1EventsByContractAddressBySignatureResponse,
GetV1EventsByContractAddressBySignatureError,
GetV1EventsByContractAddressBySignatureResponses,
GetV1EventsByContractAddressBySignatureErrors,
ThrowOnError
>({
security: [
@@ -353,8 +361,8 @@ export const getV1Transactions = <ThrowOnError extends boolean = false>(
options?: Options<GetV1TransactionsData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
GetV1TransactionsResponse,
GetV1TransactionsError,
GetV1TransactionsResponses,
GetV1TransactionsErrors,
ThrowOnError
>({
security: [
@@ -378,8 +386,8 @@ export const getV1TransactionsByContractAddress = <
options: Options<GetV1TransactionsByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1TransactionsByContractAddressResponse,
GetV1TransactionsByContractAddressError,
GetV1TransactionsByContractAddressResponses,
GetV1TransactionsByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -406,8 +414,8 @@ export const getV1TransactionsByContractAddressBySignature = <
>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1TransactionsByContractAddressBySignatureResponse,
GetV1TransactionsByContractAddressBySignatureError,
GetV1TransactionsByContractAddressBySignatureResponses,
GetV1TransactionsByContractAddressBySignatureErrors,
ThrowOnError
>({
security: [
@@ -429,8 +437,8 @@ export const getV1TokensOwners = <ThrowOnError extends boolean = false>(
options: Options<GetV1TokensOwnersData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1TokensOwnersResponse,
GetV1TokensOwnersError,
GetV1TokensOwnersResponses,
GetV1TokensOwnersErrors,
ThrowOnError
>({
security: [
@@ -457,8 +465,8 @@ export const getV1TokensTransfersTransactionByTransactionHash = <
>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1TokensTransfersTransactionByTransactionHashResponse,
GetV1TokensTransfersTransactionByTransactionHashError,
GetV1TokensTransfersTransactionByTransactionHashResponses,
GetV1TokensTransfersTransactionByTransactionHashErrors,
ThrowOnError
>({
security: [
@@ -482,8 +490,8 @@ export const getV1TokensTransfersByContractAddress = <
options: Options<GetV1TokensTransfersByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1TokensTransfersByContractAddressResponse,
GetV1TokensTransfersByContractAddressError,
GetV1TokensTransfersByContractAddressResponses,
GetV1TokensTransfersByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -505,8 +513,8 @@ export const getV1TokensTransfers = <ThrowOnError extends boolean = false>(
options?: Options<GetV1TokensTransfersData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
GetV1TokensTransfersResponse,
GetV1TokensTransfersError,
GetV1TokensTransfersResponses,
GetV1TokensTransfersErrors,
ThrowOnError
>({
security: [
@@ -521,9 +529,9 @@ export const getV1TokensTransfers = <ThrowOnError extends boolean = false>(
};
/**
* @deprecated
* Get ERC-20 balances by address
* Get ERC-20 balances for a given address. [BEING DEPRECATED IN FAVOR OF /tokens]
* @deprecated
*/
export const getV1TokensErc20ByOwnerAddress = <
ThrowOnError extends boolean = false,
@@ -531,8 +539,8 @@ export const getV1TokensErc20ByOwnerAddress = <
options: Options<GetV1TokensErc20ByOwnerAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1TokensErc20ByOwnerAddressResponse,
unknown,
GetV1TokensErc20ByOwnerAddressResponses,
GetV1TokensErc20ByOwnerAddressErrors,
ThrowOnError
>({
security: [
@@ -554,8 +562,8 @@ export const getV1Tokens = <ThrowOnError extends boolean = false>(
options: Options<GetV1TokensData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1TokensResponse,
unknown,
GetV1TokensResponses,
GetV1TokensErrors,
ThrowOnError
>({
security: [
@@ -570,9 +578,9 @@ export const getV1Tokens = <ThrowOnError extends boolean = false>(
};
/**
* @deprecated
* Get ERC-721 balances by address
* Get ERC-721 (NFT) balances for a given address [BEING DEPRECATED IN FAVOR OF /nfts/balance]
* @deprecated
*/
export const getV1TokensErc721ByOwnerAddress = <
ThrowOnError extends boolean = false,
@@ -580,8 +588,8 @@ export const getV1TokensErc721ByOwnerAddress = <
options: Options<GetV1TokensErc721ByOwnerAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1TokensErc721ByOwnerAddressResponse,
unknown,
GetV1TokensErc721ByOwnerAddressResponses,
GetV1TokensErc721ByOwnerAddressErrors,
ThrowOnError
>({
security: [
@@ -596,9 +604,9 @@ export const getV1TokensErc721ByOwnerAddress = <
};
/**
* @deprecated
* Get ERC-1155 balances by address
* Get ERC-1155 (Multi Token) balances for a given address [BEING DEPRECATED IN FAVOR OF /nfts/balance]
* @deprecated
*/
export const getV1TokensErc1155ByOwnerAddress = <
ThrowOnError extends boolean = false,
@@ -606,8 +614,8 @@ export const getV1TokensErc1155ByOwnerAddress = <
options: Options<GetV1TokensErc1155ByOwnerAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1TokensErc1155ByOwnerAddressResponse,
unknown,
GetV1TokensErc1155ByOwnerAddressResponses,
GetV1TokensErc1155ByOwnerAddressErrors,
ThrowOnError
>({
security: [
@@ -629,8 +637,8 @@ export const getV1TokensPriceSupported = <ThrowOnError extends boolean = false>(
options?: Options<GetV1TokensPriceSupportedData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
GetV1TokensPriceSupportedResponse,
unknown,
GetV1TokensPriceSupportedResponses,
GetV1TokensPriceSupportedErrors,
ThrowOnError
>({
security: [
@@ -652,8 +660,8 @@ export const getV1TokensPrice = <ThrowOnError extends boolean = false>(
options?: Options<GetV1TokensPriceData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
GetV1TokensPriceResponse,
unknown,
GetV1TokensPriceResponses,
GetV1TokensPriceErrors,
ThrowOnError
>({
security: [
@@ -675,8 +683,8 @@ export const getV1TokensLookup = <ThrowOnError extends boolean = false>(
options: Options<GetV1TokensLookupData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1TokensLookupResponse,
unknown,
GetV1TokensLookupResponses,
GetV1TokensLookupErrors,
ThrowOnError
>({
security: [
@@ -698,8 +706,8 @@ export const getV1ResolveByInput = <ThrowOnError extends boolean = false>(
options: Options<GetV1ResolveByInputData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1ResolveByInputResponse,
GetV1ResolveByInputError,
GetV1ResolveByInputResponses,
GetV1ResolveByInputErrors,
ThrowOnError
>({
security: [
@@ -721,8 +729,8 @@ export const getV1Blocks = <ThrowOnError extends boolean = false>(
options?: Options<GetV1BlocksData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
GetV1BlocksResponse,
GetV1BlocksError,
GetV1BlocksResponses,
GetV1BlocksErrors,
ThrowOnError
>({
security: [
@@ -746,8 +754,8 @@ export const getV1ContractsAbiByContractAddress = <
options: Options<GetV1ContractsAbiByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1ContractsAbiByContractAddressResponse,
GetV1ContractsAbiByContractAddressError,
GetV1ContractsAbiByContractAddressResponses,
GetV1ContractsAbiByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -771,8 +779,8 @@ export const getV1ContractsMetadataByContractAddress = <
options: Options<GetV1ContractsMetadataByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1ContractsMetadataByContractAddressResponse,
GetV1ContractsMetadataByContractAddressError,
GetV1ContractsMetadataByContractAddressResponses,
GetV1ContractsMetadataByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -796,8 +804,8 @@ export const postV1DecodeByContractAddress = <
options: Options<PostV1DecodeByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).post<
PostV1DecodeByContractAddressResponse,
PostV1DecodeByContractAddressError,
PostV1DecodeByContractAddressResponses,
PostV1DecodeByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -810,7 +818,7 @@ export const postV1DecodeByContractAddress = <
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
...options.headers,
},
});
};
@@ -825,8 +833,8 @@ export const getV1NftsBalanceByOwnerAddress = <
options: Options<GetV1NftsBalanceByOwnerAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsBalanceByOwnerAddressResponse,
unknown,
GetV1NftsBalanceByOwnerAddressResponses,
GetV1NftsBalanceByOwnerAddressErrors,
ThrowOnError
>({
security: [
@@ -850,8 +858,8 @@ export const getV1NftsCollectionsByContractAddress = <
options: Options<GetV1NftsCollectionsByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsCollectionsByContractAddressResponse,
GetV1NftsCollectionsByContractAddressError,
GetV1NftsCollectionsByContractAddressResponses,
GetV1NftsCollectionsByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -873,8 +881,8 @@ export const getV1Nfts = <ThrowOnError extends boolean = false>(
options: Options<GetV1NftsData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsResponse,
GetV1NftsError,
GetV1NftsResponses,
GetV1NftsErrors,
ThrowOnError
>({
security: [
@@ -898,8 +906,8 @@ export const getV1NftsOwnersByContractAddress = <
options: Options<GetV1NftsOwnersByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsOwnersByContractAddressResponse,
GetV1NftsOwnersByContractAddressError,
GetV1NftsOwnersByContractAddressResponses,
GetV1NftsOwnersByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -923,8 +931,8 @@ export const getV1NftsOwnersByContractAddressByTokenId = <
options: Options<GetV1NftsOwnersByContractAddressByTokenIdData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsOwnersByContractAddressByTokenIdResponse,
GetV1NftsOwnersByContractAddressByTokenIdError,
GetV1NftsOwnersByContractAddressByTokenIdResponses,
GetV1NftsOwnersByContractAddressByTokenIdErrors,
ThrowOnError
>({
security: [
@@ -946,8 +954,8 @@ export const getV1NftsTransfers = <ThrowOnError extends boolean = false>(
options?: Options<GetV1NftsTransfersData, ThrowOnError>,
) => {
return (options?.client ?? _heyApiClient).get<
GetV1NftsTransfersResponse,
GetV1NftsTransfersError,
GetV1NftsTransfersResponses,
GetV1NftsTransfersErrors,
ThrowOnError
>({
security: [
@@ -974,8 +982,8 @@ export const getV1NftsTransfersTransactionByTransactionHash = <
>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsTransfersTransactionByTransactionHashResponse,
GetV1NftsTransfersTransactionByTransactionHashError,
GetV1NftsTransfersTransactionByTransactionHashResponses,
GetV1NftsTransfersTransactionByTransactionHashErrors,
ThrowOnError
>({
security: [
@@ -999,8 +1007,8 @@ export const getV1NftsTransfersByContractAddress = <
options: Options<GetV1NftsTransfersByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsTransfersByContractAddressResponse,
GetV1NftsTransfersByContractAddressError,
GetV1NftsTransfersByContractAddressResponses,
GetV1NftsTransfersByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -1024,8 +1032,8 @@ export const getV1NftsByContractAddress = <
options: Options<GetV1NftsByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsByContractAddressResponse,
GetV1NftsByContractAddressError,
GetV1NftsByContractAddressResponses,
GetV1NftsByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -1052,8 +1060,8 @@ export const getV1NftsTransfersByContractAddressByTokenId = <
>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsTransfersByContractAddressByTokenIdResponse,
GetV1NftsTransfersByContractAddressByTokenIdError,
GetV1NftsTransfersByContractAddressByTokenIdResponses,
GetV1NftsTransfersByContractAddressByTokenIdErrors,
ThrowOnError
>({
security: [
@@ -1077,8 +1085,8 @@ export const getV1NftsByContractAddressByTokenId = <
options: Options<GetV1NftsByContractAddressByTokenIdData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsByContractAddressByTokenIdResponse,
GetV1NftsByContractAddressByTokenIdError,
GetV1NftsByContractAddressByTokenIdResponses,
GetV1NftsByContractAddressByTokenIdErrors,
ThrowOnError
>({
security: [
@@ -1102,8 +1110,8 @@ export const getV1NftsMetadataRefreshByContractAddress = <
options: Options<GetV1NftsMetadataRefreshByContractAddressData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsMetadataRefreshByContractAddressResponse,
GetV1NftsMetadataRefreshByContractAddressError,
GetV1NftsMetadataRefreshByContractAddressResponses,
GetV1NftsMetadataRefreshByContractAddressErrors,
ThrowOnError
>({
security: [
@@ -1130,8 +1138,8 @@ export const getV1NftsMetadataRefreshByContractAddressByTokenId = <
>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1NftsMetadataRefreshByContractAddressByTokenIdResponse,
GetV1NftsMetadataRefreshByContractAddressByTokenIdError,
GetV1NftsMetadataRefreshByContractAddressByTokenIdResponses,
GetV1NftsMetadataRefreshByContractAddressByTokenIdErrors,
ThrowOnError
>({
security: [
@@ -1155,8 +1163,8 @@ export const getV1WalletsByWalletAddressTransactions = <
options: Options<GetV1WalletsByWalletAddressTransactionsData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).get<
GetV1WalletsByWalletAddressTransactionsResponse,
GetV1WalletsByWalletAddressTransactionsError,
GetV1WalletsByWalletAddressTransactionsResponses,
GetV1WalletsByWalletAddressTransactionsErrors,
ThrowOnError
>({
security: [
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Config } from "@hey-api/client-fetch";
import type { Config } from "./client/client/index.js";
import { client } from "./client/client.gen.js";
export type InsightClientOptions = {
+2 -1
View File
@@ -20,7 +20,8 @@
"jsx": "react-jsx",
"lib": [
"ES2022", // By using ES2022 we get access to the `.cause` property on `Error` instances.
"DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped.
"DOM", // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped.
"DOM.Iterable" // Adding `DOM.Iterable` for the Headers.entries() method.
],
"module": "NodeNext",
+11
View File
@@ -1,5 +1,16 @@
# @thirdweb-dev/nebula
## 0.2.0
### Minor Changes
- [#7431](https://github.com/thirdweb-dev/js/pull/7431) [`1387337`](https://github.com/thirdweb-dev/js/commit/1387337955a38f1527266b0a6146bb18d86426aa) Thanks [@PaoloRollo](https://github.com/PaoloRollo)! - update hey-api version to 0.76.0
### Patch Changes
- Updated dependencies [[`1387337`](https://github.com/thirdweb-dev/js/commit/1387337955a38f1527266b0a6146bb18d86426aa), [`6712911`](https://github.com/thirdweb-dev/js/commit/67129119504953e8c547f209f184e4aeca5fe23a), [`af89836`](https://github.com/thirdweb-dev/js/commit/af89836b6c1b8ea39407fdfddc2fb07ad95d768d), [`6aa2765`](https://github.com/thirdweb-dev/js/commit/6aa2765a344d89e8072010d5aff09d3cc6a59281)]:
- thirdweb@5.105.0
## 0.1.1
### Patch Changes
+3 -3
View File
@@ -1,7 +1,7 @@
import { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
input: "https://nebula-api.thirdweb-dev.com/openapi.json",
output: { format: "biome", lint: "biome", path: "src/client" },
plugins: ["@hey-api/client-fetch"],
input: "https://nebula-api.thirdweb-dev.com/openapi.json",
output: { format: "biome", lint: "biome", path: "src/client" },
plugins: ["@hey-api/client-fetch"],
});
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/nebula",
"version": "0.1.1",
"version": "0.2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
@@ -28,12 +28,11 @@
"src/*"
],
"dependencies": {
"@hey-api/client-fetch": "0.10.0",
"thirdweb": "workspace:*"
},
"devDependencies": {
"@biomejs/biome": "2.0.4",
"@hey-api/openapi-ts": "0.72.1",
"@hey-api/openapi-ts": "0.76.0",
"rimraf": "6.0.1",
"tslib": "^2.8.1"
},
+1 -1
View File
@@ -5,7 +5,7 @@ import {
createClient,
createConfig,
type ClientOptions as DefaultClientOptions,
} from "@hey-api/client-fetch";
} from "./client/index.js";
import type { ClientOptions } from "./types.gen.js";
/**
+189
View File
@@ -0,0 +1,189 @@
import type { Client, Config, RequestOptions } from "./types.js";
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from "./utils.js";
type ReqInit = Omit<RequestInit, "body" | "headers"> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};
const interceptors = createInterceptors<
Request,
Response,
unknown,
RequestOptions
>();
const request: Client["request"] = async (options) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}
if (opts.body && opts.bodySerializer) {
opts.body = opts.bodySerializer(opts.body);
}
// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.body === "") {
opts.headers.delete("Content-Type");
}
const url = buildUrl(opts);
const requestInit: ReqInit = {
redirect: "follow",
...opts,
};
let request = new Request(url, requestInit);
for (const fn of interceptors.request._fns) {
if (fn) {
request = await fn(request, opts);
}
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response = await _fetch(request);
for (const fn of interceptors.response._fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
const result = {
request,
response,
};
if (response.ok) {
if (
response.status === 204 ||
response.headers.get("Content-Length") === "0"
) {
return opts.responseStyle === "data"
? {}
: {
data: {},
...result,
};
}
const parseAs =
(opts.parseAs === "auto"
? getParseAs(response.headers.get("Content-Type"))
: opts.parseAs) ?? "json";
let data: any;
switch (parseAs) {
case "arrayBuffer":
case "blob":
case "formData":
case "json":
case "text":
data = await response[parseAs]();
break;
case "stream":
return opts.responseStyle === "data"
? response.body
: {
data: response.body,
...result,
};
}
if (parseAs === "json") {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === "data"
? data
: {
data,
...result,
};
}
let error = await response.text();
try {
error = JSON.parse(error);
} catch {
// noop
}
let finalError = error;
for (const fn of interceptors.error._fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError;
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === "data"
? undefined
: {
error: finalError,
...result,
};
};
return {
buildUrl,
connect: (options) => request({ ...options, method: "CONNECT" }),
delete: (options) => request({ ...options, method: "DELETE" }),
get: (options) => request({ ...options, method: "GET" }),
getConfig,
head: (options) => request({ ...options, method: "HEAD" }),
interceptors,
options: (options) => request({ ...options, method: "OPTIONS" }),
patch: (options) => request({ ...options, method: "PATCH" }),
post: (options) => request({ ...options, method: "POST" }),
put: (options) => request({ ...options, method: "PUT" }),
request,
setConfig,
trace: (options) => request({ ...options, method: "TRACE" }),
};
};
@@ -0,0 +1,22 @@
export type { Auth } from "../core/auth.js";
export type { QuerySerializerOptions } from "../core/bodySerializer.js";
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from "../core/bodySerializer.js";
export { buildClientParams } from "../core/params.js";
export { createClient } from "./client.js";
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
OptionsLegacyParser,
RequestOptions,
RequestResult,
ResponseStyle,
TDataShape,
} from "./types.js";
export { createConfig, mergeHeaders } from "./utils.js";
+222
View File
@@ -0,0 +1,222 @@
import type { Auth } from "../core/auth.js";
import type {
Client as CoreClient,
Config as CoreConfig,
} from "../core/types.js";
import type { Middleware } from "./utils.js";
export type ResponseStyle = "data" | "fields";
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, "body" | "headers" | "method">,
CoreConfig {
/**
* Base URL for all requests made by this client.
*/
baseUrl?: T["baseUrl"];
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: (request: Request) => ReturnType<typeof fetch>;
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
*
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/
next?: never;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
* You can override this behavior with any of the {@link Body} methods.
* Select `stream` if you don't want to parse response data at all.
*
* @default 'auto'
*/
parseAs?:
| "arrayBuffer"
| "auto"
| "blob"
| "formData"
| "json"
| "stream"
| "text";
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
* @default 'fields'
*/
responseStyle?: ResponseStyle;
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T["throwOnError"];
}
export interface RequestOptions<
TResponseStyle extends ResponseStyle = "fields",
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends Config<{
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
}> {
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
}
export type RequestResult<
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
> = ThrowOnError extends true
? Promise<
TResponseStyle extends "data"
? TData extends Record<string, unknown>
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends "data"
?
| (TData extends Record<string, unknown>
? TData[keyof TData]
: TData)
| undefined
: (
| {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
error: undefined;
}
| {
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
}
) & {
request: Request;
response: Response;
}
>;
export interface ClientOptions {
baseUrl?: string;
responseStyle?: ResponseStyle;
throwOnError?: boolean;
}
type MethodFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
>(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type RequestFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields",
>(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method"> &
Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, "method">,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = <
TData extends {
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
url: string;
},
>(
options: Pick<TData, "url"> & Options<TData>,
) => string;
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
interceptors: Middleware<Request, Response, unknown, RequestOptions>;
};
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
export interface TDataShape {
body?: unknown;
headers?: unknown;
path?: unknown;
query?: unknown;
url: string;
}
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
> = OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
"body" | "path" | "query" | "url"
> &
Omit<TData, "url">;
export type OptionsLegacyParser<
TData = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields",
> = TData extends { body?: any }
? TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
"body" | "headers" | "url"
> &
TData
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "url"> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "headers">
: TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
"headers" | "url"
> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, "body">
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "url"> & TData;
+417
View File
@@ -0,0 +1,417 @@
import { getAuthToken } from "../core/auth.js";
import type {
QuerySerializer,
QuerySerializerOptions,
} from "../core/bodySerializer.js";
import { jsonBodySerializer } from "../core/bodySerializer.js";
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from "../core/pathSerializer.js";
import type { Client, ClientOptions, Config, RequestOptions } from "./types.js";
interface PathSerializer {
path: Record<string, unknown>;
url: string;
}
const PATH_PARAM_RE = /\{[^{}]+\}/g;
type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited";
type MatrixStyle = "label" | "matrix" | "simple";
type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = "simple";
if (name.endsWith("*")) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith(".")) {
name = name.substring(1);
style = "label";
} else if (name.startsWith(";")) {
name = name.substring(1);
style = "matrix";
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(
match,
serializeArrayParam({ explode, name, style, value }),
);
continue;
}
if (typeof value === "object") {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === "matrix") {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === "label" ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const createQuerySerializer = <T = unknown>({
allowReserved,
array,
object,
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
if (queryParams && typeof queryParams === "object") {
for (const name in queryParams) {
const value = queryParams[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved,
explode: true,
name,
style: "form",
value,
...array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === "object") {
const serializedObject = serializeObjectParam({
allowReserved,
explode: true,
name,
style: "deepObject",
value: value as Record<string, unknown>,
...object,
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join("&");
};
return querySerializer;
};
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (
contentType: string | null,
): Exclude<Config["parseAs"], "auto"> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
return "stream";
}
const cleanContent = contentType.split(";")[0]?.trim();
if (!cleanContent) {
return;
}
if (
cleanContent.startsWith("application/json") ||
cleanContent.endsWith("+json")
) {
return "json";
}
if (cleanContent === "multipart/form-data") {
return "formData";
}
if (
["application/", "audio/", "image/", "video/"].some((type) =>
cleanContent.startsWith(type),
)
) {
return "blob";
}
if (cleanContent.startsWith("text/")) {
return "text";
}
return;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, "security"> &
Pick<RequestOptions, "auth" | "query"> & {
headers: Headers;
}) => {
for (const auth of security) {
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue;
}
const name = auth.name ?? "Authorization";
switch (auth.in) {
case "query":
if (!options.query) {
options.query = {};
}
options.query[name] = token;
break;
case "cookie":
options.headers.append("Cookie", `${name}=${token}`);
break;
case "header":
default:
options.headers.set(name, token);
break;
}
return;
}
};
export const buildUrl: Client["buildUrl"] = (options) => {
const url = getUrl({
baseUrl: options.baseUrl as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === "function"
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
let url = (baseUrl ?? "") + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : "";
if (search.startsWith("?")) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseUrl?.endsWith("/")) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
export const mergeHeaders = (
...headers: Array<Required<Config>["headers"] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header || typeof header !== "object") {
continue;
}
const iterator =
header instanceof Headers ? header.entries() : Object.entries(header);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, v as string);
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
typeof value === "object" ? JSON.stringify(value) : (value as string),
);
}
}
}
return mergedHeaders;
};
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
options: Options,
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
class Interceptors<Interceptor> {
_fns: (Interceptor | null)[];
constructor() {
this._fns = [];
}
clear() {
this._fns = [];
}
getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === "number") {
return this._fns[id] ? id : -1;
} else {
return this._fns.indexOf(id);
}
}
exists(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
return !!this._fns[index];
}
eject(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = null;
}
}
update(id: number | Interceptor, fn: Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = fn;
return id;
} else {
return false;
}
}
use(fn: Interceptor) {
this._fns = [...this._fns, fn];
return this._fns.length - 1;
}
}
// `createInterceptors()` response, meant for external use as it does not
// expose internals
export interface Middleware<Req, Res, Err, Options> {
error: Pick<
Interceptors<ErrInterceptor<Err, Res, Req, Options>>,
"eject" | "use"
>;
request: Pick<Interceptors<ReqInterceptor<Req, Options>>, "eject" | "use">;
response: Pick<
Interceptors<ResInterceptor<Res, Req, Options>>,
"eject" | "use"
>;
}
// do not add `Middleware` as return type so we can use _fns internally
export const createInterceptors = <Req, Res, Err, Options>() => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
});
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: "form",
},
object: {
explode: true,
style: "deepObject",
},
});
const defaultHeaders = {
"Content-Type": "application/json",
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
parseAs: "auto",
querySerializer: defaultQuerySerializer,
...override,
});
+40
View File
@@ -0,0 +1,40 @@
export type AuthToken = string | undefined;
export interface Auth {
/**
* Which part of the request do we use to send the auth?
*
* @default 'header'
*/
in?: "header" | "query" | "cookie";
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string;
scheme?: "basic" | "bearer";
type: "apiKey" | "http";
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token =
typeof callback === "function" ? await callback(auth) : callback;
if (!token) {
return;
}
if (auth.scheme === "bearer") {
return `Bearer ${token}`;
}
if (auth.scheme === "basic") {
return `Basic ${btoa(token)}`;
}
return token;
};
@@ -0,0 +1,84 @@
import type {
ArrayStyle,
ObjectStyle,
SerializerOptions,
} from "./pathSerializer.js";
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: any) => any;
export interface QuerySerializerOptions {
allowReserved?: boolean;
array?: SerializerOptions<ArrayStyle>;
object?: SerializerOptions<ObjectStyle>;
}
const serializeFormDataPair = (data: FormData, key: string, value: unknown) => {
if (typeof value === "string" || value instanceof Blob) {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
const serializeUrlSearchParamsPair = (
data: URLSearchParams,
key: string,
value: unknown,
) => {
if (typeof value === "string") {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
) => {
const data = new FormData();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(data, key, value);
}
});
return data;
},
};
export const jsonBodySerializer = {
bodySerializer: <T>(body: T) =>
JSON.stringify(body, (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
) => {
const data = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value);
}
});
return data.toString();
},
};
+141
View File
@@ -0,0 +1,141 @@
type Slot = "body" | "headers" | "path" | "query";
export type Field =
| {
in: Exclude<Slot, "body">;
key: string;
map?: string;
}
| {
in: Extract<Slot, "body">;
key?: string;
map?: string;
};
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
}
export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = {
$body_: "body",
$headers_: "headers",
$path_: "path",
$query_: "query",
};
const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map<
string,
{
in: Slot;
map?: string;
}
>;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map();
}
for (const config of fields) {
if ("in" in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
});
}
} else if (config.args) {
buildKeyMap(config.args, map);
}
}
return map;
};
interface Params {
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === "object" && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
};
export const buildClientParams = (
args: ReadonlyArray<unknown>,
fields: FieldsConfig,
) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
};
const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index];
}
if (!config) {
continue;
}
if ("in" in config) {
if (config.key) {
const field = map.get(config.key)!;
const name = field.map || config.key;
(params[field.in] as Record<string, unknown>)[name] = arg;
} else {
params.body = arg;
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key);
if (field) {
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
} else {
const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix),
);
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[
key.slice(prefix.length)
] = value;
} else {
for (const [slot, allowed] of Object.entries(
config.allowExtra ?? {},
)) {
if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
}
}
}
}
}
}
}
stripEmptySlots(params);
return params;
};
@@ -0,0 +1,179 @@
interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
name: string;
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean;
style: T;
}
export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited";
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = "label" | "matrix" | "simple";
export type ObjectStyle = "form" | "deepObject";
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string;
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case "label":
return ".";
case "matrix":
return ";";
case "simple":
return ",";
default:
return "&";
}
};
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case "form":
return ",";
case "pipeDelimited":
return "|";
case "spaceDelimited":
return "%20";
default:
return ",";
}
};
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case "label":
return ".";
case "matrix":
return ";";
case "simple":
return ",";
default:
return "&";
}
};
export const serializeArrayParam = ({
allowReserved,
explode,
name,
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[];
}) => {
if (!explode) {
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
switch (style) {
case "label":
return `.${joinedValues}`;
case "matrix":
return `;${name}=${joinedValues}`;
case "simple":
return joinedValues;
default:
return `${name}=${joinedValues}`;
}
}
const separator = separatorArrayExplode(style);
const joinedValues = value
.map((v) => {
if (style === "label" || style === "simple") {
return allowReserved ? v : encodeURIComponent(v as string);
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
});
})
.join(separator);
return style === "label" || style === "matrix"
? separator + joinedValues
: joinedValues;
};
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return "";
}
if (typeof value === "object") {
throw new Error(
"Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.",
);
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
export const serializeObjectParam = ({
allowReserved,
explode,
name,
style,
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date;
valueOnly?: boolean;
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
}
if (style !== "deepObject" && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [
...values,
key,
allowReserved ? (v as string) : encodeURIComponent(v as string),
];
});
const joinedValues = values.join(",");
switch (style) {
case "form":
return `${name}=${joinedValues}`;
case "label":
return `.${joinedValues}`;
case "matrix":
return `;${name}=${joinedValues}`;
default:
return joinedValues;
}
}
const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === "deepObject" ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator);
return style === "label" || style === "matrix"
? separator + joinedValues
: joinedValues;
};
+98
View File
@@ -0,0 +1,98 @@
import type { Auth, AuthToken } from "./auth.js";
import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from "./bodySerializer.js";
export interface Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
> {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
connect: MethodFn;
delete: MethodFn;
get: MethodFn;
getConfig: () => Config;
head: MethodFn;
options: MethodFn;
patch: MethodFn;
post: MethodFn;
put: MethodFn;
request: RequestFn;
setConfig: (config: Config) => Config;
trace: MethodFn;
}
export interface Config {
/**
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
*
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit["headers"]
| Record<
string,
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined
| unknown
>;
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?:
| "CONNECT"
| "DELETE"
| "GET"
| "HEAD"
| "OPTIONS"
| "PATCH"
| "POST"
| "PUT"
| "TRACE";
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
* style, and reserved characters are percent-encoded.
*
* This method will have no effect if the native `paramsSerializer()` Axios
* API function is used.
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
}
+1 -1
View File
@@ -4,7 +4,7 @@ import type {
Client,
Options as ClientOptions,
TDataShape,
} from "@hey-api/client-fetch";
} from "./client/index.js";
import { client as _heyApiClient } from "./client.gen.js";
import type {
AuthDelegateLoginData,
+213 -22
View File
@@ -544,6 +544,10 @@ export type AgentResponse = {
* Is Public
*/
is_public: boolean;
/**
* Summary
*/
summary?: string | null;
context: AgentTaskContext;
/**
* Prompts
@@ -750,6 +754,58 @@ export type AgentToolDataAgent = {
agent_id: string;
};
/**
* AgentToolDataApi
*/
export type AgentToolDataApi = {
/**
* Type
*/
type?: "api";
/**
* Url
*/
url: string;
/**
* Method
*/
method?: string;
/**
* Headers
*/
headers?: {
[key: string]: string;
} | null;
/**
* Params
*/
params?: {
[key: string]: unknown;
} | null;
/**
* Task Param
*/
task_param: string;
/**
* Description
*/
description: string;
};
/**
* AgentToolDataBuiltin
*/
export type AgentToolDataBuiltin = {
/**
* Type
*/
type?: "builtin";
/**
* Agent Name
*/
agent_name: string;
};
/**
* AgentToolDataMcp
*/
@@ -762,9 +818,9 @@ export type AgentToolDataMcp = {
};
/**
* AgentToolDescription
* AgentToolReference
*/
export type AgentToolDescription = {
export type AgentToolReference = {
/**
* Agent Id
*/
@@ -777,6 +833,10 @@ export type AgentToolDescription = {
* Description
*/
description: string;
/**
* Summary
*/
summary?: string | null;
};
/**
@@ -802,9 +862,15 @@ export type AgentToolResponse = {
| ({
type: "agent";
} & AgentToolDataAgent)
| ({
type: "builtin";
} & AgentToolDataBuiltin)
| ({
type: "mcp";
} & AgentToolDataMcp);
} & AgentToolDataMcp)
| ({
type: "api";
} & AgentToolDataApi);
/**
* Created At
*/
@@ -1100,6 +1166,44 @@ export type AgentWalletResponse = {
updated_at: string;
};
/**
* ApiToolConfig
*/
export type ApiToolConfig = {
/**
* Url
*/
url: string;
/**
* Method
*/
method: string;
/**
* Headers
*/
headers?: {
[key: string]: string;
} | null;
/**
* Params
*/
params?: {
[key: string]: string;
} | null;
/**
* Task Param
*/
task_param?: string | null;
/**
* Name
*/
name?: string | null;
/**
* Description
*/
description: string;
};
/**
* BlockchainEntity
* Base class for all blockchain entities tracked in memory.
@@ -1127,6 +1231,16 @@ export type BlockchainEntity = {
last_mentioned_at?: string;
};
/**
* BuiltinAgentReference
*/
export type BuiltinAgentReference = {
/**
* Agent Name
*/
agent_name: string;
};
/**
* ChatContentImage
*/
@@ -1188,19 +1302,17 @@ export type ChatMessage = {
/**
* Content
*/
content:
| string
| Array<
| ({
type: "image";
} & ChatContentImage)
| ({
type: "text";
} & ChatContentText)
| ({
type: "transaction";
} & ChatContentTransaction)
>;
content: Array<
| ({
type: "image";
} & ChatContentImage)
| ({
type: "text";
} & ChatContentText)
| ({
type: "transaction";
} & ChatContentTransaction)
>;
};
/**
@@ -1431,11 +1543,19 @@ export type CompletionContextInput = {
/**
* Mcp Tools
*/
mcp_tools?: Array<McpTool> | null;
mcp_tools?: Array<McpToolConfig> | null;
/**
* Agent Tools
*/
agent_tools?: Array<AgentToolDescription> | null;
agent_tools?: Array<AgentToolReference> | null;
/**
* Builtin Agents
*/
builtin_agents?: Array<BuiltinAgentReference> | null;
/**
* Api Tools
*/
api_tools?: Array<ApiToolConfig> | null;
/**
* Deployed Contracts
*/
@@ -1477,6 +1597,32 @@ export type CompletionContextOutput = {
* Prompts
*/
prompts?: Array<ChatMessage>;
/**
* Mcp Tools
*/
mcp_tools?: Array<McpToolConfig> | null;
/**
* Agent Tools
*/
agent_tools?: Array<AgentToolReference> | null;
/**
* Builtin Agents
*/
builtin_agents?: Array<BuiltinAgentReference> | null;
/**
* Api Tools
*/
api_tools?: Array<ApiToolConfig> | null;
/**
* Deployed Contracts
*/
deployed_contracts?: Array<ContractBase>;
/**
* Entities
*/
entities?: {
[key: string]: BlockchainEntity;
};
};
/**
@@ -1906,9 +2052,15 @@ export type CreateAgentToolParams = {
| ({
type: "agent";
} & AgentToolDataAgent)
| ({
type: "builtin";
} & AgentToolDataBuiltin)
| ({
type: "mcp";
} & AgentToolDataMcp);
} & AgentToolDataMcp)
| ({
type: "api";
} & AgentToolDataApi);
/**
* Description
*/
@@ -2147,12 +2299,16 @@ export type McpConfig = {
headers?: {
[key: string]: unknown;
} | null;
/**
* Transport
*/
transport?: string;
};
/**
* McpTool
* McpToolConfig
*/
export type McpTool = {
export type McpToolConfig = {
/**
* Url
*/
@@ -2163,8 +2319,25 @@ export type McpTool = {
headers?: {
[key: string]: string;
} | null;
/**
* Transport
*/
transport?: string;
/**
* Media Support
*/
media_support?: Array<MediaOutputType> | null;
/**
* Auto Upload Media
*/
auto_upload_media?: boolean;
};
/**
* MediaOutputType
*/
export type MediaOutputType = "text" | "image" | "audio" | "json" | "binary";
/**
* Model
*/
@@ -2271,6 +2444,10 @@ export type RegistryAgentListResponse = {
* Description
*/
description: string;
/**
* Summary
*/
summary?: string | null;
/**
* Created At
*/
@@ -2598,6 +2775,14 @@ export type UpdateAgentHandlerParams = {
* Prompts
*/
prompts?: Array<ChatMessage> | null;
/**
* Triggers
*/
triggers?: Array<CreateAgentTriggerParams> | null;
/**
* Tools
*/
tools?: Array<CreateAgentToolParams> | null;
};
/**
@@ -2616,9 +2801,15 @@ export type UpdateAgentToolParams = {
| ({
type: "agent";
} & AgentToolDataAgent)
| ({
type: "builtin";
} & AgentToolDataBuiltin)
| ({
type: "mcp";
} & AgentToolDataMcp)
| ({
type: "api";
} & AgentToolDataApi)
)
| null;
/**
@@ -4266,5 +4457,5 @@ export type GetRegistryAgentsResponse =
GetRegistryAgentsResponses[keyof GetRegistryAgentsResponses];
export type ClientOptions = {
baseUrl: "http://localhost:4242" | (string & {});
baseUrl: "https://nebula-api.thirdweb-dev.com" | (string & {});
};
+5 -1
View File
@@ -1,5 +1,9 @@
import { type Config, createClient, createConfig } from "@hey-api/client-fetch";
import type { ThirdwebClient } from "thirdweb";
import {
type Config,
createClient,
createConfig,
} from "./client/client/index.js";
import { client } from "./client/client.gen.js";
export type NebulaClientOptions = {
+2 -1
View File
@@ -20,7 +20,8 @@
"jsx": "react-jsx",
"lib": [
"ES2022", // By using ES2022 we get access to the `.cause` property on `Error` instances.
"DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped.
"DOM", // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped.
"DOM.Iterable" // Adding `DOM.Iterable` for the Headers.entries() method.
],
"module": "NodeNext",
+10
View File
@@ -1,5 +1,15 @@
# @thirdweb-dev/service-utils
## 0.10.0
### Minor Changes
- [#7439](https://github.com/thirdweb-dev/js/pull/7439) [`c5f9921`](https://github.com/thirdweb-dev/js/commit/c5f9921dc9cf6c62d754f12ce6171d95bd87a590) Thanks [@arcoraven](https://github.com/arcoraven)! - feat: Add webhook producer to service-utils
### Patch Changes
- [#7440](https://github.com/thirdweb-dev/js/pull/7440) [`a2b737e`](https://github.com/thirdweb-dev/js/commit/a2b737e14493956dc888d94057e806bff77bacd2) Thanks [@arcoraven](https://github.com/arcoraven)! - fix: wrong casing on a field in webhook event helper
## 0.9.14
### Patch Changes
+2 -1
View File
@@ -5,6 +5,7 @@
},
"dependencies": {
"@confluentinc/kafka-javascript": "1.3.2",
"@paralleldrive/cuid2": "^2.2.2",
"aws4fetch": "1.0.20",
"zod": "3.25.62"
},
@@ -68,5 +69,5 @@
}
},
"typings": "dist/types/index.d.ts",
"version": "0.9.14"
"version": "0.10.0"
}
+1
View File
@@ -19,6 +19,7 @@ export * from "../core/usage.js";
export * from "../core/usageV2.js";
export * from "./kafka.js";
export * from "./usageV2.js";
export * from "./webhookProducer.js";
type NodeServiceConfig = CoreServiceConfig;
+51 -47
View File
@@ -6,48 +6,60 @@ import {
} from "../core/usageV2.js";
import { KafkaProducer } from "./kafka.js";
const TEAM_ID_PREFIX = "team_";
const PROJECT_ID_PREFIX = "prj_";
/**
* Creates a UsageV2Producer which opens a persistent TCP connection.
* This class is thread-safe so your service should re-use one instance.
* Creates a producer for usage events.
*
* Example:
* ```ts
* usageV2 = new UsageV2Producer(..)
* await usageV2.sendEvents(events)
* // Non-blocking:
* // void usageV2.sendEvents(events).catch((e) => console.error(e))
* const kafkaProducer = new KafkaProducer({...});
* const usageV2 = new UsageV2Producer({ kafkaProducer, source: "storage" });
* await usageV2.sendEvents(events);
* ```
*/
export class UsageV2Producer {
private kafkaProducer: KafkaProducer;
private topic: string;
constructor(config: {
/**
* A descriptive name for your service. Example: "storage-server"
*/
producerName: string;
/**
* A comma-separated list of `host[:port]` Kafka servers.
*/
kafkaServers: string;
/**
* The product where usage is coming from.
*/
source: UsageV2Source;
username: string;
password: string;
}) {
this.kafkaProducer = new KafkaProducer({
kafkaServers: config.kafkaServers,
password: config.password,
producerName: config.producerName,
username: config.username,
});
constructor(
config:
| {
/**
* Shared KafkaProducer instance.
*/
kafkaProducer: KafkaProducer;
/**
* The product where usage is coming from.
*/
source: UsageV2Source;
}
| {
/**
* A descriptive name for your service. Example: "storage-server"
*/
producerName: string;
/**
* A comma-separated list of `host[:port]` Kafka servers.
* @deprecated: Instantiate and pass in `kafkaProducer` instead.
*/
kafkaServers: string;
/**
* The product where usage is coming from.
*/
source: UsageV2Source;
username: string;
password: string;
},
) {
if ("kafkaProducer" in config) {
this.kafkaProducer = config.kafkaProducer;
} else {
this.kafkaProducer = new KafkaProducer({
kafkaServers: config.kafkaServers,
password: config.password,
producerName: config.producerName,
username: config.username,
});
}
this.topic = getTopicName(config.source);
}
@@ -61,29 +73,21 @@ export class UsageV2Producer {
* @param events
*/
async sendEvents(events: UsageV2Event[]): Promise<void> {
const parsedEvents = events.map((event) => ({
const parsedEvents: UsageV2Event[] = events.map((event) => ({
...event,
// Default to now.
created_at: event.created_at ?? new Date(),
// Default to a generated UUID.
id: event.id ?? randomUUID(),
// Remove the "prj_" prefix, if any.
project_id: event.project_id?.startsWith(PROJECT_ID_PREFIX)
? event.project_id.slice(PROJECT_ID_PREFIX.length)
// Remove the "prj_" prefix.
project_id: event.project_id?.startsWith("prj_")
? event.project_id.slice(4)
: event.project_id,
// Remove the "team_" prefix, if any.
team_id: event.team_id.startsWith(TEAM_ID_PREFIX)
? event.team_id.slice(TEAM_ID_PREFIX.length)
// Remove the "team_" prefix.
team_id: event.team_id.startsWith("team_")
? event.team_id.slice(5)
: event.team_id,
}));
await this.kafkaProducer.send(this.topic, parsedEvents);
}
/**
* Disconnects UsageV2Producer.
* Useful when shutting down the service to flush in-flight events.
*/
async disconnect() {
await this.kafkaProducer.disconnect();
}
}
@@ -0,0 +1,61 @@
import assert from "node:assert";
import { createId } from "@paralleldrive/cuid2";
import type { KafkaProducer } from "./kafka.js";
interface WebhookEvent extends Record<string, unknown> {
id?: `evt_${string}`;
teamId: string;
projectId?: string;
createdAt?: Date;
/**
* This should match your model defined in api-server.
*/
payload: Record<string, unknown>;
}
/**
* Creates a producer for webhook events.
*
* Example:
* ```ts
* const kafkaProducer = new KafkaProducer({...});
* const webhookProducer = new WebhookEventProducer({ kafkaProducer });
* await webhookProducer.sendEvents("your.topic.name", events);
* ```
*/
export class WebhookEventProducer {
private kafkaProducer: KafkaProducer;
constructor(config: { kafkaProducer: KafkaProducer }) {
this.kafkaProducer = config.kafkaProducer;
}
/**
* Emit a webhook event.
* This method may throw. To call this non-blocking:
* ```ts
* void webhookProducer.sendEvents(events).catch((e) => console.error(e))
* ```
*/
async sendEvents(topic: string, events: WebhookEvent[]): Promise<void> {
const parsedEvents: WebhookEvent[] = events.map((event) => {
assert(
event.teamId.startsWith("team_"),
"teamId must start with 'team_'",
);
assert(
!event.projectId || event.projectId.startsWith("prj_"),
"projectId must start with 'prj_'",
);
return {
...event,
// Default to now.
createdAt: event.createdAt ?? new Date(),
// Default to a generated UUID.
id: event.id ?? `evt_${createId()}`,
};
});
await this.kafkaProducer.send(topic, parsedEvents);
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
[
{
"import": "*",
"limit": "65 kB",
"limit": "68 kB",
"name": "thirdweb (esm)",
"path": "./dist/esm/exports/thirdweb.js"
},
+18
View File
@@ -1,5 +1,23 @@
# thirdweb
## 5.105.0
### Minor Changes
- [#7431](https://github.com/thirdweb-dev/js/pull/7431) [`1387337`](https://github.com/thirdweb-dev/js/commit/1387337955a38f1527266b0a6146bb18d86426aa) Thanks [@PaoloRollo](https://github.com/PaoloRollo)! - update hey-api version to 0.76.0
### Patch Changes
- [#7441](https://github.com/thirdweb-dev/js/pull/7441) [`6712911`](https://github.com/thirdweb-dev/js/commit/67129119504953e8c547f209f184e4aeca5fe23a) Thanks [@gregfromstl](https://github.com/gregfromstl)! - A number of important fixes for payment widgets
- [#7437](https://github.com/thirdweb-dev/js/pull/7437) [`af89836`](https://github.com/thirdweb-dev/js/commit/af89836b6c1b8ea39407fdfddc2fb07ad95d768d) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Handle sponsored gas on TransactionWidget
- [#7426](https://github.com/thirdweb-dev/js/pull/7426) [`6aa2765`](https://github.com/thirdweb-dev/js/commit/6aa2765a344d89e8072010d5aff09d3cc6a59281) Thanks [@gregfromstl](https://github.com/gregfromstl)! - Deprecate PayEmbed
- Updated dependencies [[`1387337`](https://github.com/thirdweb-dev/js/commit/1387337955a38f1527266b0a6146bb18d86426aa)]:
- @thirdweb-dev/insight@1.1.0
- @thirdweb-dev/engine@3.1.0
## 5.104.1
### Patch Changes
+1 -1
View File
@@ -404,5 +404,5 @@
}
},
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "5.104.1"
"version": "5.105.0"
}
-2
View File
@@ -99,7 +99,6 @@ import type { PreparedQuote, Quote } from "./types/Quote.js";
*
* @throws Will throw an error if there is an issue fetching the quote.
* @bridge Buy
* @beta
*/
export async function quote(options: quote.Options): Promise<quote.Result> {
const {
@@ -326,7 +325,6 @@ export declare namespace quote {
*
* @throws Will throw an error if there is an issue fetching the quote.
* @bridge Buy
* @beta
*/
export async function prepare(
options: prepare.Options,
+3
View File
@@ -1,4 +1,5 @@
import type { Address as ox__Address } from "ox";
import { defineChain } from "../chains/utils.js";
import type { ThirdwebClient } from "../client/client.js";
import { getThirdwebBaseUrl } from "../utils/domains.js";
import { getClientFetch } from "../utils/fetch.js";
@@ -237,6 +238,8 @@ export async function prepare(
originAmount: BigInt(step.originAmount),
transactions: step.transactions.map((tx) => ({
...tx,
chain: defineChain(tx.chainId),
client,
value: tx.value ? BigInt(tx.value) : undefined,
})),
}));
@@ -326,7 +326,7 @@ export async function getBuyWithFiatQuote(
maxSteps: 2,
onramp: onrampProvider,
onrampChainId: params.onrampChainId,
onrampTokenAddress: params.onrampTokenAddress ?? NATIVE_TOKEN_ADDRESS,
onrampTokenAddress: params.onrampTokenAddress,
paymentLinkId: params.paymentLinkId,
purchaseData: params.purchaseData,
receiver: params.toAddress, // force onramp to native token to avoid missing gas issues
@@ -237,6 +237,7 @@ export function useStepExecutor(
if (tx.action === "approval" || tx.action === "fee") {
// don't poll status for approval transactions, just wait for confirmation
await waitForReceipt(result);
await new Promise((resolve) => setTimeout(resolve, 2000)); // Add an extra 2 second delay for RPC to catch up to new state
return;
}
@@ -14,6 +14,8 @@ import type { PreparedTransaction } from "../../../transaction/prepare-transacti
import { getTransactionGasCost } from "../../../transaction/utils.js";
import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-value.js";
import { toTokens } from "../../../utils/units.js";
import type { Wallet } from "../../../wallets/interfaces/wallet.js";
import { hasSponsoredTransactionsEnabled } from "../../../wallets/smart/is-smart-wallet.js";
import {
formatCurrencyAmount,
formatTokenAmount,
@@ -40,6 +42,7 @@ interface TransactionDetails {
interface UseTransactionDetailsOptions {
transaction: PreparedTransaction;
client: ThirdwebClient;
wallet: Wallet | undefined;
}
/**
@@ -49,8 +52,10 @@ interface UseTransactionDetailsOptions {
export function useTransactionDetails({
transaction,
client,
wallet,
}: UseTransactionDetailsOptions) {
const chainMetadata = useChainMetadata(transaction.chain);
const hasSponsoredTransactions = hasSponsoredTransactionsEnabled(wallet);
return useQuery({
enabled: !!transaction.to && !!chainMetadata.data,
@@ -76,7 +81,9 @@ export function useTransactionDetails({
erc20Value ? erc20Value.tokenAddress : NATIVE_TOKEN_ADDRESS,
transaction.chain.id,
).catch(() => null),
getTransactionGasCost(transaction).catch(() => null),
hasSponsoredTransactions
? 0n
: getTransactionGasCost(transaction).catch(() => null),
]);
// Process function info from ABI if available
@@ -172,6 +179,7 @@ export function useTransactionDetails({
transaction.to,
transaction.chain.id,
transaction.erc20Value?.toString(),
hasSponsoredTransactions,
],
});
}
@@ -217,6 +217,7 @@ export function BridgeOrchestrator({
{/* Error Banner */}
{state.value === "error" && state.context.currentError && (
<ErrorBanner
client={client}
error={state.context.currentError}
onCancel={onCancel}
onRetry={handleRetry}
@@ -282,6 +283,7 @@ export function BridgeOrchestrator({
amount={state.context.destinationAmount}
client={client}
destinationToken={state.context.destinationToken}
mode={uiOptions.mode}
onBack={() => {
send({ type: "BACK" });
}}
@@ -332,6 +334,7 @@ export function BridgeOrchestrator({
state.context.quote &&
state.context.completedStatuses && (
<SuccessScreen
client={client}
completedStatuses={state.context.completedStatuses}
onDone={handleBuyComplete}
preparedQuote={state.context.quote}
@@ -1,6 +1,7 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { trackPayEvent } from "../../../../analytics/track/pay.js";
import type { Token } from "../../../../bridge/index.js";
import type { Chain } from "../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../client/client.js";
@@ -260,14 +261,24 @@ type UIOptionsResult =
*
* Refer to the [`BuyWidgetConnectOptions`](https://portal.thirdweb.com/references/typescript/v5/BuyWidgetConnectOptions) type for more details.
*
* @bridge
* @beta
* @react
* @bridge Widgets
*/
export function BuyWidget(props: BuyWidgetProps) {
const localeQuery = useConnectLocale(props.locale || "en_US");
const theme = props.theme || "dark";
useQuery({
queryFn: () => {
trackPayEvent({
client: props.client,
event: "ub:ui:buy_widget:render",
toChainId: props.chain.id,
toToken: props.tokenAddress,
});
},
queryKey: ["buy_widget:render"],
});
const bridgeDataQuery = useQuery({
queryFn: async (): Promise<UIOptionsResult> => {
if (
@@ -343,7 +354,13 @@ export function BuyWidget(props: BuyWidgetProps) {
);
} else if (bridgeDataQuery.data?.type === "unsupported_token") {
// Show unsupported token screen
content = <UnsupportedTokenScreen chain={bridgeDataQuery.data.chain} />;
content = (
<UnsupportedTokenScreen
chain={bridgeDataQuery.data.chain}
client={props.client}
tokenAddress={props.tokenAddress}
/>
);
} else if (bridgeDataQuery.data?.type === "success") {
// Show normal bridge orchestrator
content = (
@@ -1,6 +1,7 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { trackPayEvent } from "../../../../analytics/track/pay.js";
import type { Token } from "../../../../bridge/index.js";
import type { Chain } from "../../../../chains/types.js";
import type { ThirdwebClient } from "../../../../client/client.js";
@@ -243,14 +244,24 @@ type UIOptionsResult =
*
* Refer to the [`CheckoutWidgetConnectOptions`](https://portal.thirdweb.com/references/typescript/v5/CheckoutWidgetConnectOptions) type for more details.
*
* @bridge
* @beta
* @react
* @bridge Widgets
*/
export function CheckoutWidget(props: CheckoutWidgetProps) {
const localeQuery = useConnectLocale(props.locale || "en_US");
const theme = props.theme || "dark";
useQuery({
queryFn: () => {
trackPayEvent({
client: props.client,
event: "ub:ui:checkout_widget:render",
toChainId: props.chain.id,
toToken: props.tokenAddress,
});
},
queryKey: ["checkout_widget:render"],
});
const bridgeDataQuery = useQuery({
queryFn: async (): Promise<UIOptionsResult> => {
const token = await getToken(
@@ -306,7 +317,13 @@ export function CheckoutWidget(props: CheckoutWidgetProps) {
);
} else if (bridgeDataQuery.data?.type === "unsupported_token") {
// Show unsupported token screen
content = <UnsupportedTokenScreen chain={bridgeDataQuery.data.chain} />;
content = (
<UnsupportedTokenScreen
chain={bridgeDataQuery.data.chain}
client={props.client}
tokenAddress={props.tokenAddress}
/>
);
} else if (bridgeDataQuery.data?.type === "success") {
// Show normal bridge orchestrator
content = (

Some files were not shown because too many files have changed in this diff Show More