Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ba92cd2cbe Version Packages (#6575)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-29 03:41:26 -07:00
Joaquim VergesandGitHub c3d0ea0059 [PayEmbed] Respect supportedTokens prop (#6580) 2025-03-29 03:35:22 -07:00
MananTank 35c5f57179 [TOOL-3839] Fix double scrollbars in project settings page (#6579) 2025-03-29 07:51:23 +00:00
gregandGitHub 2a1f4be48b Fix: Not Found Status should show Pending (#6578) 2025-03-28 20:11:43 -07:00
gregandGitHub 0121ccd702 fix: reduce cost of playground commerce example (#6577) 2025-03-28 19:19:03 -07:00
Toomas OosaluandGitHub 12bcfc596a insight webhooks docs (#6574) 2025-03-29 11:11:25 +13:00
MananTank a4ff738268 [TOOL-3833] Dashboard: Fix overflow scroll created by sr-only element (#6576) 2025-03-28 19:17:40 +00:00
YashandGitHub b49c191355 Support stylus contracts publish and deploy (#6495) 2025-03-29 07:34:37 +13:00
saminaandGitHub 06f0765665 Update page.mdx (#6572)
Signed-off-by: samina <[email protected]>
2025-03-27 19:54:50 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
12adbd8314 Version Packages (#6570)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-27 17:25:33 -07:00
saminacodes 48e014d426 added FAQ (#6567)
<!--

## 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 new details to the `page.mdx` file, providing guidance on how to enhance the performance of the `Nebula` character and improve its responses.

### Detailed summary
- Added a section on how to make `Nebula` play a certain character better, including:
  - Defining the role clearly.
  - Avoiding personality traits in responses.
  - Setting output expectations.
  - Preloading context for specific domains.
  - Utilizing session handling features.

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

<!-- end pr-codex -->
2025-03-27 23:41:37 +00:00
saminacodes 826748bb5d [Portal] fee-changes (#6573)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the fee structure and related information for the `Buy With Crypto` and swap transactions in the `fee-sharing` documentation.

### Detailed summary
- Removed references to a 1% fee and 70% sharing for `Buy With Crypto`.
- Updated the protocol fee to 0.3% for swap transactions.
- Allowed developers to set their own additional fees.
- Clarified fee structure for buying with crypto and swap transactions.

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

<!-- end pr-codex -->
2025-03-27 22:54:25 +00:00
gregfromstl d4f5a9db12 [Portal] Add cross-chain swapping guide and add bridge submodules (#6571)
<!--

## 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 documentation and functionality surrounding the `thirdweb` Universal Bridge, particularly for cross-chain swapping and related features. It updates guides, introduces new functionalities, and improves the clarity of the code.

### Detailed summary
- Updated `yarn` command in `page.mdx` for installing `thirdweb`.
- Renamed the guide from "Build a Custom Experience" to "Build a Custom Onramp Experience" in `sidebar.tsx`.
- Added a new guide for "Cross-Chain Swapping" in `page.mdx`.
- Enhanced tag filtering in `slugs.ts` to include `@bridge`.
- Updated comments in `Buy.ts` and `Sell.ts` to specify `@bridge Buy` and `@bridge Sell`.
- Improved sidebar link generation in `getSidebarLinkGroups.ts` to include `@bridge` links.
- Added comprehensive steps for cross-chain swapping in the new guide, including installation, obtaining quotes, preparing transactions, and checking swap status.

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

<!-- end pr-codex -->
2025-03-27 19:07:18 +00:00
MananTank 3b827ea32c [NEB-148] Do not add active chain to nebula context by default (#6564) 2025-03-27 18:58:20 +00:00
MananTank 2231db7980 [NEB-125] Nebula: Show popular chains at the top of the list (#6565) 2025-03-27 18:49:02 +00:00
MananTank f9872f442c [TOOL-3761] Dashboard: Use new sidebar layout in project pages (#6551)
<!--

## 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 primarily focuses on enhancing the layout and styling of the application, particularly around sidebar and project-related components. It introduces new sidebar features, refines the UI, and improves mobile responsiveness.

### Detailed summary
- Removed unused files: `tabs.tsx`, `layout.tsx` in `connect`, and `settings/layout.tsx`.
- Simplified layout in `DeployedContractsPage.tsx` by removing unnecessary divs.
- Added `AnnouncementBanner` to multiple layouts for notifications.
- Introduced `useActiveSidebarLink` for better sidebar link management.
- Enhanced `Sidebar` components with new features and improved styling.
- Added `Header` component in `ProjectOverviewPage` for better title display.
- Updated Tailwind CSS configuration to include sidebar styles.
- Improved mobile responsiveness with `useIsMobile` hook.
- Cleaned up `AnnouncementBanner` logic and structure.

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

<!-- end pr-codex -->
2025-03-27 18:38:45 +00:00
Joe-Thirdweb 473a25237e [Dashboard] reactive(1597) chainlist page update (#6566)
Added custom chain page info for Reactive Network

* Banner image
* CTA image
* About section

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces new assets and functionality for a "Reactive Mainnet" section in the dashboard. It adds images and a call-to-action that promotes Web3 automation features.

### Detailed summary
- Added `reactiveBanner.jpg` and `reactiveCTA.jpg` assets.
- Introduced a new section for "Reactive Mainnet" in `chainMetaRecord`:
  - Set `headerImgUrl` to `reactiveBanner.src`.
  - Added an `about` description for Web3 automation.
  - Configured `cta` with `backgroundImageUrl`, `title`, `buttonLink`, and `buttonText`.

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

<!-- end pr-codex -->
2025-03-27 18:23:19 +00:00
Manan TankandGitHub 2d4c5ff07c Fix "Cannot read properties of undefined (reading 'forwardRef')" error on certain bundler setups (#6569) 2025-03-27 10:41:53 -07:00
saminacodes ab738eb7ba new narrative (#6563)
<!--

## 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 updating the `AnnouncementBanner` and `Banner` components to promote the new `Universal Bridge` feature, enhancing the messaging around payment integration, and refining the content in the documentation to reflect these changes.

### Detailed summary
- Updated `AnnouncementBanner` with new `href`, `label`, and `trackingLabel`.
- Changed `Banner` ID, `text`, and `href` to reflect the `Universal Bridge` launch.
- Revised documentation for webhook events to include `Universal Bridge`.
- Enhanced content in the overview page for `Universal Bridge`.
- Updated descriptions and titles for multiple `FeatureCard` components.
- Removed fee-sharing section and added new integration options for `Universal Bridge`.

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

<!-- end pr-codex -->
2025-03-27 14:55:00 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8e6651e591 Version Packages (#6546)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-26 11:37:15 -07:00
gregandGitHub e5026a10ab [SDK] Fix: Account for bigint values in engine typed data (#6562) 2025-03-26 11:10:51 -07:00
MananTank 9c1c5f2fc8 [TOOL-3789] Dashboard: Replace simplehash with insight (#6554) 2025-03-26 14:49:27 +00:00
Phillip HoandGitHub 45cdb86911 chore: hide Engine projects from list (#6560) 2025-03-26 22:05:00 +08:00
Joaquim VergesandGitHub fe3351ce79 [TypeDoc] Remove Gist update from workflow (#6559) 2025-03-26 02:45:20 -07:00
Joaquim VergesandGitHub fc657e0b89 [Portal] Redesign documentation homepage with SDK cards and product icons (#6558) 2025-03-26 02:37:35 -07:00
Phillip HoandGitHub cff2481c06 chore: changset for make req optional when calling usage from cloudflare (#6557) 2025-03-26 12:14:05 +08:00
Phillip HoandGitHub e0e31826a8 chore: make req optional when calling usage from cloudflare (#6556) 2025-03-26 11:57:03 +08:00
Joaquim VergesandGitHub fe8b3ce5a5 [Playground] Rename "Fund Wallet" to "Buy Crypto" across pay components (#6555) 2025-03-25 17:48:21 -07:00
kumaryash90 3c120a7584 [SDK] Functions to manage extensions on a dynamic contract (#6550)
<!--

## 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 `thirdweb` library by adding functionality to manage extensions on dynamic contracts, including the ability to install and uninstall extensions.

### Detailed summary
- Introduced `uninstallExtension` function to remove extensions from dynamic contracts.
- Added `installPublishedExtension` function to install extensions with various options.
- Updated `generateExtensionFunctionsFromAbi` to use a specific string format for `functionSelector`.
- Enhanced tests for `uninstallExtension` and `installPublishedExtension` to ensure proper functionality.
- Updated generated files for `addExtension` and `removeExtension` methods with new parameter types and encoding functions.

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

<!-- end pr-codex -->
2025-03-26 00:24:19 +00:00
MananTank b51157c0ff [TOOL-3735] Fix minor typos (#6531)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on fixing typos and improving text clarity in various components of the application.

### Detailed summary
- In `PayConfig.tsx`, corrected "will are" to "are" in the `bottomText` prop.
- In `page.tsx` (commerce), corrected "paymets" to "payments".
- In `page.tsx` (headless), corrected "Seemlessly" to "Seamlessly" in the `description`.
- In `page.tsx` (embed), corrected "Seemlessly" to "Seamlessly" in the `description`.

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

<!-- end pr-codex -->
2025-03-25 22:48:05 +00:00
MananTank f1df19e612 [TOOL-3689] Dashboard: Keep switch in disabled state if field value is empty in Sponsorship rules form (#6535)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on improving the `transformedQueryData` computation by filtering out empty values from the `policy` properties. It enhances data handling for `allowedContractAddresses`, `allowedWallets`, `blockedWallets`, and `allowedChainIds`, ensuring cleaner and more accurate data is returned.

### Detailed summary
- Refactored `transformedQueryData` to filter out empty values from `allowedContractAddresses`, `allowedWallets`, `blockedWallets`, and `allowedChainIds`.
- Simplified return conditions for these properties.
- Changed handling of `allowedChainIds` to return an empty array instead of null to prevent API issues.

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

<!-- end pr-codex -->
2025-03-25 22:39:38 +00:00
MananTank 98781531bb [TOOL-3786] Portal: Improve llms.txt, llms-full.txt (#6532) 2025-03-26 03:59:07 +05:30
saminacodes d3a5ea7833 fixed broken links (#6553)
<!--

## 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 updating documentation and modifying the sidebar structure in the `Nebula` application, including changes to links and adding a new FAQ section.

### Detailed summary
- Added a new FAQ section in `faqs/page.mdx` about using Nebula in a stateless manner.
- Changed the sidebar entry from `MCP Server` to `MCP Clients` in `sidebar.tsx`.
- Updated the link for `Examples` in `sidebar.tsx` to point to a GitHub repository.

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

<!-- end pr-codex -->
2025-03-25 20:43:24 +00:00
MananTank 1a9448e152 Portal: Fix lint error (#6552)
<!--

## 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 restructuring the `sidebar` object in the `apps/portal/src/app/nebula/sidebar.tsx` file, ensuring proper formatting of the sidebar items and improving the overall readability of the code.

### Detailed summary
- Adjusted the formatting of the `sidebar` entries for `MCP Server` and `Tools`.
- Converted closing brackets from separate lines to inline, enhancing code clarity.
- Ensured consistent object structure for items in the `sidebar`.

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

<!-- end pr-codex -->
2025-03-25 19:36:16 +00:00
saminaandGitHub 2fe16119f5 [Portal] AI updated docs (#6536) 2025-03-25 02:33:38 -07:00
Joaquim VergesandGitHub c549d5934e [Bridge] Skip tests without TW_SECRET_KEY environment variable (#6545) 2025-03-25 02:32:53 -07:00
Joaquim VergesandGitHub dd65506dfb [Playground] Refactor Universal Bridge sidebar links and update order (#6543) 2025-03-24 19:30:22 -07:00
MananTank 26cfd1b30e [TOOL-3682] Dashboard: Change deploy contract status for in-app wallet (#6533) 2025-03-25 01:57:05 +00:00
MananTank 495d8edfa2 [TOOL-3757] Dashboard: Prefill last used project in contract deployment page (#6534) 2025-03-25 01:49:04 +00:00
kumaryash90 88c213b6d5 Handle default extensions (#6497)
<!--

## 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 deployment and transaction management of dynamic contracts within the `thirdweb` framework, adding support for extensions and improving the handling of contract metadata.

### Detailed summary
- Improved return formatting in `bootstrap.ts`.
- Updated `platformFeeBps` handling in `custom-contract.tsx`.
- Added tests for dynamic contract transactions in `get-required-transactions.test.ts`.
- Enhanced logic for deploying extensions in `deploy-published.ts`.
- Introduced new functions for managing dynamic contract extensions in `get-required-transactions.ts`.
- Updated `deploy-marketplace.ts` to include dynamic extensions in marketplace contracts.

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

<!-- end pr-codex -->
2025-03-24 17:21:13 +00:00
Joaquim VergesandGitHub d9091bd2e7 [Dashboard] Disable engineCloud service by default (#6530) 2025-03-24 02:31:52 -07:00
166 changed files with 5404 additions and 1739 deletions
-20
View File
@@ -28,23 +28,3 @@ jobs:
- name: Run TypeDoc
run: pnpm typedoc
- name: Update Gist
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ secrets.GIST_TOKEN }}
script: |
const fs = require('fs');
const content = fs.readFileSync('./packages/thirdweb/typedoc/parsed.json', 'utf8');
const gistId = '678fe1f331a01270bb002fee660f285d';
await github.rest.gists.update({
gist_id: gistId,
files: {
'data.json': {
content: content
}
}
});
console.log(`Permalink: https://gist.githubusercontent.com/raw/${gistId}/data.json`);
+120 -31
View File
@@ -10,12 +10,9 @@ import {
isMoralisSupported,
transformMoralisResponseToNFT,
} from "lib/wallet/nfts/moralis";
import {
generateSimpleHashUrl,
isSimpleHashSupported,
transformSimpleHashResponseToNFT,
} from "lib/wallet/nfts/simpleHash";
import type { WalletNFT } from "lib/wallet/nfts/types";
import { getVercelEnv } from "../../lib/vercel-utils";
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "../constants/env";
type WalletNFTApiReturn =
| { result: WalletNFT[]; error?: undefined }
@@ -24,40 +21,20 @@ type WalletNFTApiReturn =
export async function getWalletNFTs(params: {
chainId: number;
owner: string;
isInsightSupported: boolean;
}): Promise<WalletNFTApiReturn> {
const { chainId, owner } = params;
const supportedChainSlug = await isSimpleHashSupported(chainId);
if (supportedChainSlug && process.env.SIMPLEHASH_API_KEY) {
const url = generateSimpleHashUrl({ chainSlug: supportedChainSlug, owner });
if (params.isInsightSupported) {
const response = await getWalletNFTsFromInsight({ chainId, owner });
const response = await fetch(url, {
method: "GET",
headers: {
"X-API-KEY": process.env.SIMPLEHASH_API_KEY,
},
next: {
revalidate: 10, // cache for 10 seconds
},
});
if (response.status >= 400) {
if (!response.ok) {
return {
error: response.statusText,
error: response.error,
};
}
try {
const parsedResponse = await response.json();
const result = await transformSimpleHashResponseToNFT(
parsedResponse,
owner,
);
return { result };
} catch {
return { error: "error parsing response" };
}
return { result: response.data };
}
if (isAlchemySupported(chainId)) {
@@ -115,3 +92,115 @@ export async function getWalletNFTs(params: {
return { error: "unsupported chain" };
}
type OwnedNFTInsightResponse = {
name: string;
description: string;
image_url: string;
background_color: string;
external_url: string;
metadata_url: string;
extra_metadata: {
customImage?: string;
customAnimationUrl?: string;
animation_original_url?: string;
image_original_url?: string;
};
collection: {
name: string;
description: string;
extra_metadata: Record<string, string>;
};
contract: {
chain_id: number;
address: string;
type: "erc1155" | "erc721";
name: string;
};
owner_addresses: string[];
token_id: string;
balance: string;
token_type: "erc1155" | "erc721";
};
async function getWalletNFTsFromInsight(params: {
chainId: number;
owner: string;
}): Promise<
| {
data: WalletNFT[];
ok: true;
}
| {
ok: false;
error: string;
}
> {
const { chainId, owner } = params;
const thirdwebDomain =
getVercelEnv() === "production" ? "thirdweb" : "thirdweb-dev";
const url = new URL(`https://insight.${thirdwebDomain}.com/v1/nfts`);
url.searchParams.append("chain", chainId.toString());
url.searchParams.append("limit", "10");
url.searchParams.append("owner_address", owner);
const response = await fetch(url, {
headers: {
"x-client-id": DASHBOARD_THIRDWEB_CLIENT_ID,
},
});
if (!response.ok) {
const errorMessage = await response.text();
return {
ok: false,
error: errorMessage,
};
}
const nftsResponse = (await response.json()) as {
data: OwnedNFTInsightResponse[];
};
const isDev = getVercelEnv() !== "production";
// NOTE: ipfscdn.io/ to thirdwebstorage-dev.com/ replacement is temporary
// This should be fixed in the insight dev endpoint
const walletNFTs = nftsResponse.data.map((nft) => {
const walletNFT: WalletNFT = {
id: nft.token_id,
contractAddress: nft.contract.address,
metadata: {
uri: isDev
? nft.metadata_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/")
: nft.metadata_url,
name: nft.name,
description: nft.description,
image: isDev
? nft.image_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/")
: nft.image_url,
animation_url: isDev
? nft.extra_metadata.animation_original_url?.replace(
"ipfscdn.io/",
"thirdwebstorage-dev.com/",
)
: nft.extra_metadata.animation_original_url,
external_url: nft.external_url,
background_color: nft.background_color,
},
owner: params.owner,
tokenURI: nft.metadata_url,
type: nft.token_type === "erc721" ? "ERC721" : "ERC1155",
supply: nft.balance,
};
return walletNFT;
});
return {
data: walletNFTs,
ok: true,
};
}
@@ -19,30 +19,7 @@ export function MobileSidebar(props: {
triggerClassName?: string;
}) {
const [isOpen, setIsOpen] = useState(false);
const pathname = usePathname();
const activeLink = useMemo(() => {
function isActive(link: SidebarBaseLink) {
if (link.exactMatch) {
return link.href === pathname;
}
return pathname?.startsWith(link.href);
}
for (const link of props.links) {
if ("group" in link) {
for (const subLink of link.links) {
if (isActive(subLink)) {
return subLink;
}
}
} else {
if (isActive(link)) {
return link;
}
}
}
}, [props.links, pathname]);
const activeLink = useActiveSidebarLink(props.links);
const defaultTrigger = (
<Button
@@ -75,3 +52,32 @@ export function MobileSidebar(props: {
</Dialog>
);
}
export function useActiveSidebarLink(links: SidebarLink[]) {
const pathname = usePathname();
const activeLink = useMemo(() => {
function isActive(link: SidebarBaseLink) {
if (link.exactMatch) {
return link.href === pathname;
}
return pathname?.startsWith(link.href);
}
for (const link of links) {
if ("group" in link) {
for (const subLink of link.links) {
if (isActive(subLink)) {
return subLink;
}
}
} else if ("href" in link) {
if (isActive(link)) {
return link;
}
}
}
}, [links, pathname]);
return activeLink;
}
@@ -18,17 +18,39 @@ export function MultiNetworkSelector(props: {
onChange: (chainIds: number[]) => void;
disableChainId?: boolean;
className?: string;
priorityChains?: number[];
}) {
const { allChains, idToChain } = useAllChainsData();
const options = useMemo(() => {
return allChains.map((chain) => {
let sortedChains = allChains;
if (props.priorityChains) {
const priorityChainsSet = new Set();
for (const chainId of props.priorityChains || []) {
priorityChainsSet.add(chainId);
}
const priorityChains = (props.priorityChains || [])
.map((chainId) => {
return idToChain.get(chainId);
})
.filter((v) => !!v);
const otherChains = allChains.filter(
(chain) => !priorityChainsSet.has(chain.chainId),
);
sortedChains = [...priorityChains, ...otherChains];
}
return sortedChains.map((chain) => {
return {
label: cleanChainName(chain.name),
value: String(chain.chainId),
};
});
}, [allChains]);
}, [allChains, props.priorityChains, idToChain]);
const searchFn = useCallback(
(option: Option, searchValue: string) => {
@@ -2,11 +2,13 @@ import { ExternalLinkIcon } from "lucide-react";
import type React from "react";
import { cn } from "../../lib/utils";
import { NavLink } from "../ui/NavLink";
import { Separator } from "../ui/separator";
export type SidebarBaseLink = {
href: string;
label: React.ReactNode;
exactMatch?: boolean;
icon?: React.FC<{ className?: string }>;
tracking?: {
category: string;
action: string;
@@ -19,6 +21,9 @@ export type SidebarLink =
| {
group: string;
links: SidebarBaseLink[];
}
| {
separator: true;
};
type SidebarContentProps = {
@@ -27,21 +32,18 @@ type SidebarContentProps = {
className?: string;
};
export function Sidebar(props: SidebarContentProps) {
export function CustomSidebar(props: SidebarContentProps) {
return (
<aside
className={cn(
"sticky top-0 hidden w-[230px] flex-shrink-0 self-start lg:block",
props.className,
)}
>
<div className="py-7">
{props.header}
<div className="flex flex-col gap-1">
<RenderSidebarLinks links={props.links} />
<div className={cn("hidden w-[230px] shrink-0 lg:block", props.className)}>
<aside className="sticky top-0 self-start">
<div className="py-7">
{props.header}
<div className="flex flex-col gap-1">
<RenderSidebarLinks links={props.links} />
</div>
</div>
</div>
</aside>
</aside>
</div>
);
}
@@ -61,6 +63,10 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
);
}
if ("separator" in link) {
return <Separator className="my-2" />;
}
const isExternal = link.href.startsWith("http");
return (
<NavLink
@@ -68,9 +74,10 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
key={i}
href={link.href}
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-accent"
activeClassName="text-foreground"
activeClassName="text-foreground bg-accent"
exactMatch={link.exactMatch}
>
{link.icon && <link.icon className="size-4" />}
{link.label}
{isExternal && <ExternalLinkIcon className="size-3" />}
</NavLink>
@@ -1,6 +1,22 @@
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
import { cn } from "../../lib/utils";
import { MobileSidebar } from "./MobileSidebar";
import { Sidebar, type SidebarLink } from "./Sidebar";
import { NavLink } from "../ui/NavLink";
import { Separator } from "../ui/separator";
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
import { CustomSidebar, type SidebarLink } from "./Sidebar";
export function SidebarLayout(props: {
sidebarLinks: SidebarLink[];
@@ -17,7 +33,10 @@ export function SidebarLayout(props: {
props.className,
)}
>
<Sidebar links={sidebarLinks} className={props.desktopSidebarClassName} />
<CustomSidebar
links={sidebarLinks}
className={props.desktopSidebarClassName}
/>
<MobileSidebar
links={sidebarLinks}
triggerClassName={props.mobileSidebarClassName}
@@ -29,3 +48,118 @@ export function SidebarLayout(props: {
</div>
);
}
export function FullWidthSidebarLayout(props: {
contentSidebarLinks: SidebarLink[];
footerSidebarLinks?: SidebarLink[];
children: React.ReactNode;
className?: string;
footer?: React.ReactNode;
}) {
const { contentSidebarLinks, children, footerSidebarLinks } = props;
return (
<div
className={cn(
"relative flex w-full flex-1 overflow-y-hidden",
props.className,
)}
>
{/* left - sidebar */}
<Sidebar collapsible="icon" side="left">
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<RenderSidebarGroup
sidebarLinks={contentSidebarLinks}
groupName={undefined}
/>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
{footerSidebarLinks && (
<SidebarFooter className="pb-3">
<RenderSidebarGroup
sidebarLinks={footerSidebarLinks}
groupName={undefined}
/>
</SidebarFooter>
)}
<SidebarRail />
</Sidebar>
{/* right - content */}
<div className="flex h-full flex-grow flex-col overflow-y-auto">
<MobileSidebarTrigger
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
/>
<main className="container z-0 flex min-w-0 max-w-[1280px] grow flex-col pb-20 max-sm:w-full lg:pt-6">
{children}
</main>
{props.footer}
</div>
</div>
);
}
function RenderSidebarGroup(props: {
sidebarLinks: SidebarLink[];
groupName: string | undefined;
}) {
const { sidebarLinks } = props;
const sidebar = useSidebar();
return (
<SidebarMenu className="gap-1.5">
{sidebarLinks.map((link) => {
if ("href" in link) {
return (
<SidebarMenuItem key={link.href}>
<SidebarMenuButton asChild>
<NavLink
href={link.href}
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
activeClassName="text-foreground bg-accent"
exactMatch={link.exactMatch}
tracking={link.tracking}
onClick={() => {
sidebar.setOpenMobile(false);
}}
>
{link.icon && <link.icon className="size-4" />}
<span>{link.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
if ("separator" in link) {
return <SidebarSeparator className="my-1" />;
}
return (
<RenderSidebarGroup
sidebarLinks={link.links}
groupName={link.group}
key={link.group}
/>
);
})}
</SidebarMenu>
);
}
function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
const activeLink = useActiveSidebarLink(props.links);
return (
<div className="mb-4 flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<SidebarTrigger className="size-4" />
<Separator orientation="vertical" className="h-4" />
{activeLink && <span className="text-sm">{activeLink.label}</span>}
</div>
);
}
@@ -15,6 +15,7 @@ export type NavButtonProps = {
action: string;
label: string;
};
onClick?: () => void;
};
export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
@@ -30,7 +31,9 @@ export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
href={props.href}
className={cn(props.className, isActive && props.activeClassName)}
target={props.href.startsWith("http") ? "_blank" : undefined}
prefetch={false}
onClick={() => {
props.onClick?.();
if (props.tracking) {
track({
category: props.tracking.category,
@@ -96,7 +96,10 @@ const BreadcrumbEllipsis = ({
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
className={cn(
"relative flex h-9 w-9 items-center justify-center",
className,
)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
@@ -94,7 +94,10 @@ const PaginationEllipsis = ({
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
className={cn(
"relative flex h-9 w-9 items-center justify-center",
className,
)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
@@ -0,0 +1,773 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import { PanelLeft } from "lucide-react";
import * as React from "react";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref,
) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
// eslint-disable-next-line react-compiler/react-compiler
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open);
}, [isMobile, setOpen]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className,
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
},
);
SidebarProvider.displayName = "SidebarProvider";
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}
>(
(
{
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
},
ref,
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className,
)}
ref={ref}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="group peer relative hidden flex-shrink-0 text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
)}
/>
<div
className={cn(
"absolute inset-y-0 z-10 hidden h-full w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
);
},
);
Sidebar.displayName = "Sidebar";
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
});
SidebarTrigger.displayName = "SidebarTrigger";
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"-translate-x-1/2 group-data-[side=left]:-right-4 absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
});
SidebarRail.displayName = "SidebarRail";
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"main">
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex w-full flex-1 flex-col bg-background",
"md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}
{...props}
/>
);
});
SidebarInset.displayName = "SidebarInset";
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className,
)}
{...props}
/>
);
});
SidebarInput.displayName = "SidebarInput";
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
});
SidebarHeader.displayName = "SidebarHeader";
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
});
SidebarFooter.displayName = "SidebarFooter";
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
);
});
SidebarSeparator.displayName = "SidebarSeparator";
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
});
SidebarContent.displayName = "SidebarContent";
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
});
SidebarGroup.displayName = "SidebarGroup";
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div";
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 text-xs outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
);
});
SidebarGroupLabel.displayName = "SidebarGroupLabel";
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:-inset-2 after:absolute after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
});
SidebarGroupAction.displayName = "SidebarGroupAction";
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
));
SidebarGroupContent.displayName = "SidebarGroupContent";
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
));
SidebarMenu.displayName = "SidebarMenu";
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
));
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref,
) => {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
);
},
);
SidebarMenuButton.displayName = "SidebarMenuButton";
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:-inset-2 after:absolute after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className,
)}
{...props}
/>
);
});
SidebarMenuAction.displayName = "SidebarMenuAction";
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 font-medium text-sidebar-foreground text-xs tabular-nums",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
));
SidebarMenuBadge.displayName = "SidebarMenuBadge";
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean;
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
});
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-sidebar-border border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
));
SidebarMenuSub.displayName = "SidebarMenuSub";
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />);
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
});
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}
@@ -5,6 +5,7 @@ import invariant from "tiny-invariant";
export function useWalletNFTs(params: {
chainId: number;
walletAddress?: string;
isInsightSupported: boolean;
}) {
return useQuery({
queryKey: ["walletNfts", params.chainId, params.walletAddress],
@@ -13,6 +14,7 @@ export function useWalletNFTs(params: {
return getWalletNFTs({
chainId: params.chainId,
owner: params.walletAddress,
isInsightSupported: params.isInsightSupported,
});
},
enabled: !!params.walletAddress,
@@ -28,7 +28,7 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
<div className="h-6" />
<h2 className="px-4 text-center font-semibold text-lg tracking-tight">
Buy Funds on {sanitizedChainName} using thirdweb Pay
Buy Funds on {sanitizedChainName} using Universal Bridge
</h2>
<div className="h-2" />
@@ -50,8 +50,9 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
<Link
href="https://portal.thirdweb.com/connect/pay/overview"
className="inline-flex items-center gap-1.5 text-muted-foreground text-sm hover:text-foreground"
target="_blank"
>
Learn more about thirdweb Pay
Learn more about Universal Bridge
<ExternalLinkIcon className="size-3" />
</Link>
</div>
@@ -13,7 +13,6 @@ import { ListerOnly } from "@3rdweb-sdk/react/components/roles/lister-only";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { isAlchemySupported } from "lib/wallet/nfts/alchemy";
import { isMoralisSupported } from "lib/wallet/nfts/moralis";
import { useSimplehashSupport } from "lib/wallet/nfts/simpleHash";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import type { ThirdwebContract } from "thirdweb";
@@ -25,6 +24,7 @@ interface CreateListingButtonProps {
createText?: string;
type?: "direct-listings" | "english-auctions";
twAccount: Account | undefined;
isInsightSupported: boolean;
}
const LISTING_MODES = ["Select NFT", "Manual"] as const;
@@ -34,6 +34,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
type,
contract,
twAccount,
isInsightSupported,
...restButtonProps
}) => {
const address = useActiveAccount()?.address;
@@ -41,13 +42,12 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
const [listingMode, setListingMode] =
useState<(typeof LISTING_MODES)[number]>("Select NFT");
const simplehashQuery = useSimplehashSupport(contract.chain.id);
const isSupportedChain =
contract.chain.id &&
(simplehashQuery.data ||
(isInsightSupported ||
isAlchemySupported(contract.chain.id) ||
isMoralisSupported(contract.chain.id));
return (
<ListerOnly contract={contract}>
<Sheet open={open} onOpenChange={setOpen}>
@@ -83,6 +83,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
actionText={createText}
setOpen={setOpen}
mode={listingMode === "Select NFT" ? "automatic" : "manual"}
isInsightSupported={isInsightSupported}
/>
</div>
</>
@@ -95,6 +96,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
actionText={createText}
setOpen={setOpen}
mode="manual"
isInsightSupported={isInsightSupported}
/>
</div>
)}
@@ -21,7 +21,6 @@ import { useAllChainsData } from "hooks/chains/allChains";
import { useTxNotifications } from "hooks/useTxNotifications";
import { isAlchemySupported } from "lib/wallet/nfts/alchemy";
import { isMoralisSupported } from "lib/wallet/nfts/moralis";
import { useSimplehashSupport } from "lib/wallet/nfts/simpleHash";
import type { WalletNFT } from "lib/wallet/nfts/types";
import { CircleAlertIcon, InfoIcon } from "lucide-react";
import Link from "next/link";
@@ -87,6 +86,7 @@ type CreateListingsFormProps = {
mode: "automatic" | "manual";
type?: "direct-listings" | "english-auctions";
twAccount: Account | undefined;
isInsightSupported: boolean;
};
const auctionTimes = [
@@ -106,16 +106,17 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
setOpen,
twAccount,
mode,
isInsightSupported,
}) => {
const trackEvent = useTrack();
const chainId = contract.chain.id;
const { idToChain } = useAllChainsData();
const network = idToChain.get(chainId);
const [isFormLoading, setIsFormLoading] = useState(false);
const simplehashQuery = useSimplehashSupport(contract.chain.id);
const isSupportedChain =
chainId &&
(!!simplehashQuery.data ||
(isInsightSupported ||
isAlchemySupported(chainId) ||
isMoralisSupported(chainId));
@@ -124,7 +125,9 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
const { data: walletNFTs, isPending: isWalletNFTsLoading } = useWalletNFTs({
chainId,
walletAddress: account?.address,
isInsightSupported,
});
const sendAndConfirmTx = useSendAndConfirmTransaction();
const listingNotifications = useTxNotifications(
"NFT listed Successfully",
@@ -29,6 +29,7 @@ export function ContractDirectListingsPageClient(props: {
<ContractDirectListingsPage
contract={props.contract}
twAccount={props.twAccount}
isInsightSupported={metadataQuery.data.isInsightSupported}
/>
);
}
@@ -8,17 +8,19 @@ import { DirectListingsTable } from "./components/table";
interface ContractDirectListingsPageProps {
contract: ThirdwebContract;
twAccount: Account | undefined;
isInsightSupported: boolean;
}
export const ContractDirectListingsPage: React.FC<
ContractDirectListingsPageProps
> = ({ contract, twAccount }) => {
> = ({ contract, twAccount, isInsightSupported }) => {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-row items-center justify-between">
<p className="text-lg">Contract Listings</p>
<div className="flex flex-row gap-4">
<CreateListingButton
isInsightSupported={isInsightSupported}
contract={contract}
type="direct-listings"
createText="Create Direct Listing"
@@ -29,15 +29,18 @@ export default async function Page(props: {
);
}
const { isDirectListingSupported } = await getContractPageMetadata(
info.contract,
);
const { isDirectListingSupported, isInsightSupported } =
await getContractPageMetadata(info.contract);
if (!isDirectListingSupported) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
return (
<ContractDirectListingsPage contract={info.contract} twAccount={account} />
<ContractDirectListingsPage
contract={info.contract}
twAccount={account}
isInsightSupported={isInsightSupported}
/>
);
}
@@ -29,6 +29,7 @@ export function ContractEnglishAuctionsPageClient(props: {
<ContractEnglishAuctionsPage
contract={props.contract}
twAccount={props.twAccount}
isInsightSupported={metadataQuery.data.isInsightSupported}
/>
);
}
@@ -8,11 +8,12 @@ import { EnglishAuctionsTable } from "./components/table";
interface ContractEnglishAuctionsProps {
contract: ThirdwebContract;
twAccount: Account | undefined;
isInsightSupported: boolean;
}
export const ContractEnglishAuctionsPage: React.FC<
ContractEnglishAuctionsProps
> = ({ contract, twAccount }) => {
> = ({ contract, twAccount, isInsightSupported }) => {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-row items-center justify-between">
@@ -23,6 +24,7 @@ export const ContractEnglishAuctionsPage: React.FC<
type="english-auctions"
createText="Create English Auction"
twAccount={twAccount}
isInsightSupported={isInsightSupported}
/>
</div>
</div>
@@ -30,9 +30,8 @@ export default async function Page(props: {
);
}
const { isEnglishAuctionSupported } = await getContractPageMetadata(
info.contract,
);
const { isEnglishAuctionSupported, isInsightSupported } =
await getContractPageMetadata(info.contract);
if (!isEnglishAuctionSupported) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
@@ -42,6 +41,7 @@ export default async function Page(props: {
<ContractEnglishAuctionsPage
contract={info.contract}
twAccount={twAccount}
isInsightSupported={isInsightSupported}
/>
);
}
@@ -21,7 +21,7 @@ import {
} from "./detectedFeatures/permissions";
import { supportedERCs } from "./detectedFeatures/supportedERCs";
import { type EmbedTypeToShow, getEmbedTypeToShow } from "./getEmbedTypeToShow";
import { isAnalyticsSupportedForChain } from "./isAnalyticsSupportedForChain";
import { isInsightSupportedForChain } from "./isAnalyticsSupportedForChain";
export type ContractPageMetadata = {
supportedERCs: {
@@ -36,7 +36,7 @@ export type ContractPageMetadata = {
isPermissionsEnumerableSupported: boolean;
isModularCore: boolean;
embedType: EmbedTypeToShow;
isAnalyticsSupported: boolean;
isInsightSupported: boolean;
isSplitSupported: boolean;
isERC721ClaimConditionsSupported: boolean;
isERC20ClaimConditionsSupported: boolean;
@@ -47,7 +47,7 @@ export type ContractPageMetadata = {
};
export async function getContractPageMetadata(contract: ThirdwebContract) {
return getContractPageMetadataSetup(contract, isAnalyticsSupportedForChain);
return getContractPageMetadataSetup(contract, isInsightSupportedForChain);
}
export async function getContractPageMetadataSetup(
@@ -56,7 +56,7 @@ export async function getContractPageMetadataSetup(
): Promise<ContractPageMetadata> {
const [
functionSelectorsResult,
isAnalyticsSupportedResult,
isInsightSupportedResult,
contractTypeResult,
] = await Promise.allSettled([
resolveFunctionSelectors(contract),
@@ -69,9 +69,9 @@ export async function getContractPageMetadataSetup(
? functionSelectorsResult.value
: [];
const isAnalyticsSupported =
isAnalyticsSupportedResult.status === "fulfilled"
? isAnalyticsSupportedResult.value
const isInsightSupported =
isInsightSupportedResult.status === "fulfilled"
? isInsightSupportedResult.value
: true;
const contractType =
@@ -86,7 +86,7 @@ export async function getContractPageMetadataSetup(
isPermissionsEnumerableSupported(functionSelectors),
isModularCore: isModularCoreContract(functionSelectors),
embedType: getEmbedTypeToShow(functionSelectors),
isAnalyticsSupported: isAnalyticsSupported,
isInsightSupported: isInsightSupported,
isSplitSupported: contractType === "Split",
isVoteContract: contractType === "VoteERC20",
isERC721ClaimConditionsSupported:
@@ -47,7 +47,7 @@ export function getContractPageSidebarLinks(data: {
{
label: "Analytics",
href: `${layoutPrefix}/analytics`,
hide: !data.metadata.isAnalyticsSupported,
hide: !data.metadata.isInsightSupported,
exactMatch: true,
},
{
@@ -4,7 +4,7 @@ import { getVercelEnv } from "lib/vercel-utils";
const thirdwebDomain =
getVercelEnv() !== "production" ? "thirdweb-dev" : "thirdweb";
export async function isAnalyticsSupportedForChain(
export async function isInsightSupportedForChain(
chainId: number,
): Promise<boolean> {
try {
@@ -32,6 +32,7 @@ export function AccountPageClient(props: {
contract={props.contract}
chainMetadata={props.chainMetadata}
twAccount={props.twAccount}
isInsightSupported={metadataQuery.data.isInsightSupported}
/>
);
}
@@ -12,12 +12,14 @@ interface AccountPageProps {
contract: ThirdwebContract;
chainMetadata: ChainMetadata;
twAccount: Account | undefined;
isInsightSupported: boolean;
}
export const AccountPage: React.FC<AccountPageProps> = ({
contract,
chainMetadata,
twAccount,
isInsightSupported,
}) => {
const symbol = chainMetadata.nativeCurrency.symbol || "Native Token";
@@ -43,7 +45,7 @@ export const AccountPage: React.FC<AccountPageProps> = ({
<div className="flex flex-row items-center justify-between">
<Heading size="title.sm">NFTs owned</Heading>
</div>
<NftsOwned contract={contract} />
<NftsOwned contract={contract} isInsightSupported={isInsightSupported} />
</div>
);
};
@@ -6,12 +6,17 @@ import { NFTCards } from "../../_components/NFTCards";
interface NftsOwnedProps {
contract: ThirdwebContract;
isInsightSupported: boolean;
}
export const NftsOwned: React.FC<NftsOwnedProps> = ({ contract }) => {
export const NftsOwned: React.FC<NftsOwnedProps> = ({
contract,
isInsightSupported,
}) => {
const { data: walletNFTs, isPending: isWalletNFTsLoading } = useWalletNFTs({
chainId: contract.chain.id,
walletAddress: contract.address,
isInsightSupported: isInsightSupported,
});
const nfts = walletNFTs?.result || [];
@@ -33,7 +33,8 @@ export default async function Page(props: {
);
}
const { isAccount } = await getContractPageMetadata(contract);
const { isAccount, isInsightSupported } =
await getContractPageMetadata(contract);
if (!isAccount) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
@@ -44,6 +45,7 @@ export default async function Page(props: {
contract={contract}
chainMetadata={chainMetadata}
twAccount={account}
isInsightSupported={isInsightSupported}
/>
);
}
@@ -21,7 +21,7 @@ export function ContractAnalyticsPageClient(props: {
return <ErrorPage />;
}
if (!metadataQuery.data.isAnalyticsSupported) {
if (!metadataQuery.data.isInsightSupported) {
return <RedirectToContractOverview contract={props.contract} />;
}
@@ -34,9 +34,9 @@ export default async function Page(props: {
);
}
const { isAnalyticsSupported } = await getContractPageMetadata(info.contract);
const { isInsightSupported } = await getContractPageMetadata(info.contract);
if (!isAnalyticsSupported) {
if (!isInsightSupported) {
redirect(`/${params.chain_id}/${params.contractAddress}`);
}
@@ -34,7 +34,7 @@ export function ContractOverviewPageClient(props: {
contractPageMetadata.isPermissionsEnumerableSupported
}
chainSlug={chainMetadata.slug}
isAnalyticsSupported={contractPageMetadata.isAnalyticsSupported}
isAnalyticsSupported={contractPageMetadata.isInsightSupported}
functionSelectors={contractPageMetadata.functionSelectors}
// TODO - create a fully client rendered version of publishedBy and ContractCard and plug it here
publishedBy={undefined}
@@ -44,7 +44,7 @@ export default async function Page(props: {
contractPageMetadata.isPermissionsEnumerableSupported
}
chainSlug={chainMetadata.slug}
isAnalyticsSupported={contractPageMetadata.isAnalyticsSupported}
isAnalyticsSupported={contractPageMetadata.isInsightSupported}
functionSelectors={contractPageMetadata.functionSelectors}
publishedBy={
<ErrorBoundary fallback={null}>
Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

@@ -47,6 +47,8 @@ import metalBanner from "./temp-assets/metalBanner.png";
import plumeBanner from "./temp-assets/plumeBanner.png";
import plumeCTA from "./temp-assets/plumeCTA.png";
import reactiveBanner from "./temp-assets/reactiveBanner.jpg";
import reactiveCTA from "./temp-assets/reactiveCTA.jpg";
import rivalzBanner from "./temp-assets/rivalzBanner.png";
import rivalzCTA from "./temp-assets/rivalzCTA.png";
import rootStockBanner from "./temp-assets/rootstock-banner.png";
@@ -575,6 +577,18 @@ const chainMetaRecord = {
buttonText: "Learn More",
},
},
//Reactive Mainnet
1597: {
headerImgUrl: reactiveBanner.src,
about:
"Web3’s first IFTTT infrastructure. Automate multi-chain workflows with event-driven logic – execute actions without compromising decentralization.",
cta: {
backgroundImageUrl: reactiveCTA.src,
title: "Turn Smart Contracts Reactive",
buttonLink: "https://dev.reactive.network",
buttonText: "Learn More",
},
},
//Saakuru Mainnet
7225878: {
headerImgUrl: saakuruBanner.src,
@@ -13,7 +13,7 @@ export function DeployedContractsPageHeader(props: {
const [importModalOpen, setImportModalOpen] = useState(false);
return (
<div className="border-b">
<div>
<ImportModal
isOpen={importModalOpen}
onClose={() => {
@@ -23,27 +23,25 @@ export function DeployedContractsPageHeader(props: {
projectId={props.projectId}
/>
<div className="container">
<div className="flex flex-col gap-3 py-10 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="font-semibold text-3xl tracking-tight">Contracts</h1>
</div>
<div className="flex gap-3 [&>*]:grow">
<Button
className="gap-2 bg-card"
variant="outline"
onClick={() => setImportModalOpen(true)}
>
<DownloadIcon className="size-4" />
Import contract
</Button>
<Button asChild className="gap-2">
<Link href="/explore">
<PlusIcon className="size-4" />
Deploy contract
</Link>
</Button>
</div>
<div className="flex flex-col gap-3 pb-6 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="font-semibold text-3xl tracking-tight">Contracts</h1>
</div>
<div className="flex gap-3 [&>*]:grow">
<Button
className="gap-2 bg-card"
variant="outline"
onClick={() => setImportModalOpen(true)}
>
<DownloadIcon className="size-4" />
Import contract
</Button>
<Button asChild className="gap-2">
<Link href="/explore">
<PlusIcon className="size-4" />
Deploy contract
</Link>
</Button>
</div>
</div>
</div>
@@ -12,13 +12,12 @@ export function DeployedContractsPage(props: {
authToken: string;
}) {
return (
<div className="flex grow flex-col pb-20">
<div className="flex grow flex-col">
<DeployedContractsPageHeader
teamId={props.teamId}
projectId={props.projectId}
/>
<div className="h-10" />
<div className="container flex grow flex-col">
<div className="flex grow flex-col">
<Suspense fallback={<Loading />}>
<DeployedContractsPageAsync {...props} />
</Suspense>
@@ -4,6 +4,7 @@ import { AppFooter } from "@/components/blocks/app-footer";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import type React from "react";
import { TabPathLinks } from "../../@/components/ui/tabs";
import { AnnouncementBanner } from "../../components/notices/AnnouncementBanner";
import { getAuthTokenWalletAddress } from "../api/lib/getAuthToken";
import { TWAutoConnect } from "../components/autoconnect";
import { loginRedirect } from "../login/loginRedirect";
@@ -53,6 +54,7 @@ async function HeaderAndNav(props: {
return (
<div className="bg-card">
<AnnouncementBanner />
<AccountHeader
teamsAndProjects={teamsAndProjects}
account={props.twAccount}
@@ -1,34 +0,0 @@
import { isSimpleHashSupported } from "lib/wallet/nfts/simpleHash";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export const runtime = "edge";
export const GET = async (req: NextRequest) => {
const searchParams = req.nextUrl.searchParams;
const chainIdStr = searchParams.get("chainId");
if (!chainIdStr) {
return NextResponse.json(
{
error: "Missing chain ID parameter.",
},
{ status: 400 },
);
}
const chainId = Number.parseInt(chainIdStr);
if (!Number.isInteger(chainId)) {
return NextResponse.json(
{
error: "Invalid chain ID parameter.",
},
{ status: 400 },
);
}
const found = await isSimpleHashSupported(chainId);
return NextResponse.json({ result: found });
};
+1 -3
View File
@@ -1,4 +1,4 @@
import "@/styles/globals.css";
import "../global.css";
import { DashboardRouterTopProgressBar } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import type { Metadata } from "next";
@@ -6,7 +6,6 @@ import PlausibleProvider from "next-plausible";
import { Inter } from "next/font/google";
import NextTopLoader from "nextjs-toploader";
import { Suspense } from "react";
import { OrganizeContractsToProjectsBanner } from "../components/notices/AnnouncementBanner";
import { OpCreditsGrantedModalWrapperServer } from "../components/onboarding/OpCreditsGrantedModalWrapperServer";
import { PosthogIdentifierServer } from "../components/wallets/PosthogIdentifierServer";
import { EnsureValidConnectedWalletLoginServer } from "./components/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer";
@@ -72,7 +71,6 @@ export default function RootLayout({
fontSans.variable,
)}
>
<OrganizeContractsToProjectsBanner />
<AppRouterProviders>
{children}
<Suspense fallback={null}>
@@ -13,7 +13,7 @@ import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react";
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
import { useActiveAccount } from "thirdweb/react";
import { type NebulaContext, promptNebula } from "../api/chat";
import { createSession, updateSession } from "../api/session";
import type { SessionInfo } from "../api/types";
@@ -32,7 +32,6 @@ export function ChatPageContent(props: {
initialPrompt: string | undefined;
}) {
const address = useActiveAccount()?.address;
const activeChain = useActiveWalletChain();
const client = useThirdwebClient(props.authToken);
const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false);
const [messages, setMessages] = useState<Array<ChatMessage>>(() => {
@@ -133,14 +132,9 @@ export function ChatPageContent(props: {
// ignore local storage errors
}
// else - use the active chain
updatedContextFilters.chainIds = activeChain
? [activeChain.id.toString()]
: [];
return updatedContextFilters;
});
}, [address, isNewSession, hasUserUpdatedContextFilters, activeChain]);
}, [address, isNewSession, hasUserUpdatedContextFilters]);
const [sessionId, _setSessionId] = useState<string | undefined>(
props.session?.id,
@@ -199,6 +199,17 @@ export function ContextFiltersForm(props: {
onChange={(values) => {
form.setValue("chainIds", values.join(","));
}}
priorityChains={[
1, // ethereum
56, // bnb smart chain mainnet (bsc)
42161, // arbitrum one mainnet
8453, // base mainnet
43114, // avalanche mainnet
146, // sonic
137, // polygon
80094, // berachain mainnet
10, // optimism
]}
/>
</FormControl>
<FormMessage />
@@ -1,7 +1,9 @@
import { getProjects } from "@/api/projects";
import { getTeamNebulaWaitList, getTeams } from "@/api/team";
import { AppFooter } from "@/components/blocks/app-footer";
import { TabPathLinks } from "@/components/ui/tabs";
import { redirect } from "next/navigation";
import { AnnouncementBanner } from "../../../../components/notices/AnnouncementBanner";
import { getValidAccount } from "../../../account/settings/getAccount";
import { getAuthTokenWalletAddress } from "../../../api/lib/getAuthToken";
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
@@ -42,6 +44,7 @@ export default async function TeamLayout(props: {
return (
<div className="flex h-full grow flex-col">
<AnnouncementBanner />
<div className="bg-card">
<TeamHeaderLoggedIn
currentTeam={team}
@@ -92,6 +95,7 @@ export default async function TeamLayout(props: {
</div>
<main className="flex grow flex-col">{props.children}</main>
<AppFooter />
</div>
);
}
@@ -75,6 +75,11 @@ export function TeamProjectsPage(props: {
);
}
// @TODO: HACK hide Engine projects from the list.
_projectsToShow = _projectsToShow.filter(
(project) => !project.name.startsWith("Cloud-hosted Engine ("),
);
return _projectsToShow;
}, [searchTerm, sortBy, projects]);
@@ -0,0 +1,97 @@
"use client";
import { FullWidthSidebarLayout } from "@/components/blocks/SidebarLayout";
import { AppFooter } from "@/components/blocks/app-footer";
import {
BookTextIcon,
BoxIcon,
HomeIcon,
SettingsIcon,
WalletIcon,
} from "lucide-react";
import { ContractIcon } from "../../../../(dashboard)/(chain)/components/server/icons/ContractIcon";
import { InsightIcon } from "../../../../(dashboard)/(chain)/components/server/icons/InsightIcon";
import { PayIcon } from "../../../../(dashboard)/(chain)/components/server/icons/PayIcon";
import { SmartAccountIcon } from "../../../../(dashboard)/(chain)/components/server/icons/SmartAccountIcon";
export function ProjectSidebarLayout(props: {
layoutPath: string;
children: React.ReactNode;
}) {
const { layoutPath, children } = props;
const tracking = (label: string) => ({
category: "project-sidebar",
action: "click",
label,
});
return (
<FullWidthSidebarLayout
footer={<AppFooter />}
contentSidebarLinks={[
{
href: layoutPath,
exactMatch: true,
label: "Overview",
icon: HomeIcon,
tracking: tracking("overview"),
},
{
label: "In-App Wallets",
href: `${layoutPath}/connect/in-app-wallets`,
icon: WalletIcon,
tracking: tracking("in-app-wallets"),
},
{
label: "Account Abstraction",
href: `${layoutPath}/connect/account-abstraction`,
icon: SmartAccountIcon,
tracking: tracking("account-abstraction"),
},
{
label: "Universal Bridge",
href: `${layoutPath}/connect/universal-bridge`,
icon: PayIcon,
tracking: tracking("universal-bridge"),
},
{
href: `${layoutPath}/contracts`,
label: "Contracts",
icon: ContractIcon,
tracking: tracking("contracts"),
},
{
href: `${layoutPath}/insight`,
label: "Insight",
icon: InsightIcon,
tracking: tracking("insight"),
},
]}
footerSidebarLinks={[
{
href: `${layoutPath}/settings`,
label: "Project Settings",
icon: SettingsIcon,
tracking: tracking("project-settings"),
},
{
separator: true,
},
{
href: "https://portal.thirdweb.com",
label: "Documentation",
icon: BookTextIcon,
tracking: tracking("documentation"),
},
{
href: "https://playground.thirdweb.com/connect/sign-in/button",
label: "Playground",
icon: BoxIcon,
tracking: tracking("playground"),
},
]}
>
{children}
</FullWidthSidebarLayout>
);
}
@@ -0,0 +1,23 @@
"use client";
import { useEffect } from "react";
export const LAST_USED_PROJECT_ID = "last-used-project-id";
export const LAST_USED_TEAM_ID = "last-used-team-id";
export function SaveLastUsedProject(props: {
projectId: string;
teamId: string;
}) {
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
try {
localStorage.setItem(LAST_USED_PROJECT_ID, props.projectId);
localStorage.setItem(LAST_USED_TEAM_ID, props.teamId);
} catch {
// ignore localStorage errors
}
}, [props.projectId, props.teamId]);
return null;
}
@@ -1,33 +0,0 @@
import type { SidebarLink } from "@/components/blocks/Sidebar";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
export default async function Layout(props: {
params: Promise<{
team_slug: string;
project_slug: string;
}>;
children: React.ReactNode;
}) {
const { team_slug, project_slug } = await props.params;
const links: SidebarLink[] = [
{
label: "In-App Wallets",
href: `/team/${team_slug}/${project_slug}/connect/in-app-wallets`,
},
{
label: "Account Abstraction",
href: `/team/${team_slug}/${project_slug}/connect/account-abstraction`,
},
{
label: "Universal Bridge",
href: `/team/${team_slug}/${project_slug}/connect/universal-bridge`,
},
{
label: "Playground",
href: "https://playground.thirdweb.com/connect/sign-in/button",
},
];
return <SidebarLayout sidebarLinks={links}>{props.children}</SidebarLayout>;
}
@@ -10,15 +10,11 @@ import Link from "next/link";
export default async function Page() {
return (
<div className="flex grow flex-col">
<div className="border-border border-b py-10">
<div className="container flex max-w-[1000px] flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<h1 className="font-semibold text-2xl tracking-tight sm:text-3xl">
Insight
</h1>
</div>
</div>
<h1 className="mb-6 font-semibold text-2xl tracking-tight sm:text-3xl">
Insight
</h1>
<div className="container max-w-[1000px] py-8 pb-20">
<div className="pb-20">
<BlueprintCard />
</div>
</div>
@@ -1,10 +1,13 @@
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import { SidebarProvider } from "@/components/ui/sidebar";
import { redirect } from "next/navigation";
import { AnnouncementBanner } from "../../../../components/notices/AnnouncementBanner";
import { getValidAccount } from "../../../account/settings/getAccount";
import { getAuthTokenWalletAddress } from "../../../api/lib/getAuthToken";
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
import { ProjectTabs } from "./tabs";
import { ProjectSidebarLayout } from "./components/ProjectSidebarLayout";
import { SaveLastUsedProject } from "./components/SaveLastUsedProject";
export default async function TeamLayout(props: {
children: React.ReactNode;
@@ -46,21 +49,26 @@ export default async function TeamLayout(props: {
redirect(`/team/${params.team_slug}`);
}
const layoutPath = `/team/${params.team_slug}/${params.project_slug}`;
return (
<div className="flex grow flex-col">
<div className="bg-card">
<TeamHeaderLoggedIn
currentProject={project}
currentTeam={team}
teamsAndProjects={teamsAndProjects}
account={account}
accountAddress={accountAddress}
/>
<ProjectTabs
layoutPath={`/team/${params.team_slug}/${params.project_slug}`}
/>
<SidebarProvider>
<div className="flex h-dvh min-w-0 grow flex-col">
<div className="sticky top-0 z-10 border-border border-b bg-card">
<AnnouncementBanner />
<TeamHeaderLoggedIn
currentProject={project}
currentTeam={team}
teamsAndProjects={teamsAndProjects}
account={account}
accountAddress={accountAddress}
/>
</div>
<ProjectSidebarLayout layoutPath={layoutPath}>
{props.children}
</ProjectSidebarLayout>
</div>
<div className="flex grow flex-col">{props.children}</div>
</div>
<SaveLastUsedProject projectId={project.id} teamId={team.id} />
</SidebarProvider>
);
}
@@ -29,13 +29,14 @@ import {
getChainMetadata,
} from "thirdweb/chains";
import { type WalletId, getWalletInfo } from "thirdweb/wallets";
import { AnalyticsHeader } from "../../components/Analytics/AnalyticsHeader";
import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChartCard";
import { PieChartCard } from "../../components/Analytics/PieChartCard";
import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX";
import { RpcMethodBarChartCard } from "./components/RpcMethodBarChartCard";
import { TransactionsCharts } from "./components/Transactions";
import { RangeSelector } from "components/analytics/range-selector";
interface PageParams {
team_slug: string;
project_slug: string;
@@ -80,8 +81,8 @@ export default async function ProjectOverviewPage(props: PageProps) {
return (
<div className="flex grow flex-col">
<div className="w-full border-border border-b">
<AnalyticsHeader
<div className="w-full">
<Header
title={project.name}
interval={interval}
range={range}
@@ -90,11 +91,11 @@ export default async function ProjectOverviewPage(props: PageProps) {
</div>
{!isActive ? (
<div className="container py-8 pb-20">
<div className="pt-6">
<ProjectFTUX project={project} teamSlug={params.team_slug} />
</div>
) : (
<div className="container flex grow flex-col py-6">
<div className="flex grow flex-col pt-6">
<Suspense fallback={<GenericLoadingPage />}>
<ProjectAnalytics
project={project}
@@ -189,7 +190,7 @@ async function ProjectAnalytics(props: {
link="https://portal.thirdweb.com/connect/quickstart"
/>
)}
<div className="grid gap-6 max-md:px-6 md:grid-cols-2">
<div className="grid gap-6 md:grid-cols-2">
{walletConnections.status === "fulfilled" &&
walletConnections.value.length > 0 ? (
<WalletDistributionCard data={walletConnections.value} />
@@ -461,3 +462,23 @@ async function TotalSponsoredCard({
/>
);
}
export function Header(props: {
title: string;
interval: "day" | "week";
range: Range;
showRangeSelector: boolean;
}) {
const { title, interval, range, showRangeSelector } = props;
return (
<div className="flex flex-col items-start gap-3 md:flex-row md:items-center">
<div className="flex-1">
<h1 className="font-semibold text-2xl tracking-tight md:text-3xl">
{title}
</h1>
</div>
{showRangeSelector && <RangeSelector interval={interval} range={range} />}
</div>
);
}
@@ -7,14 +7,10 @@ export default async function Layout(props: {
}) {
return (
<div className="flex grow flex-col">
<div className="border-border border-b py-10">
<div className="container max-w-[1000px]">
<h1 className="font-semibold text-3xl tracking-tight">
Project Settings
</h1>
</div>
</div>
<div className="container max-w-[1000px] py-6">{props.children}</div>
<h1 className="font-semibold text-3xl tracking-tight">
Project Settings
</h1>
<div className="py-6">{props.children}</div>
</div>
);
}
@@ -1,41 +0,0 @@
"use client";
import { TabPathLinks } from "@/components/ui/tabs";
export function ProjectTabs(props: {
layoutPath: string;
}) {
const { layoutPath } = props;
return (
<TabPathLinks
tabContainerClassName="px-4 lg:px-6"
links={[
{
path: layoutPath,
exactMatch: true,
name: "Overview",
},
{
// directly link to /in-app-wallets to skip 1 redirect and use `isActive` for checking if the tab is active or not
path: `${layoutPath}/connect/in-app-wallets`,
name: "Connect",
isActive: (path) => path.startsWith(`${layoutPath}/connect`),
},
{
path: `${layoutPath}/contracts`,
name: "Contracts",
},
{
path: `${layoutPath}/insight`,
name: "Insight",
},
{
path: `${layoutPath}/settings`,
name: "Settings",
},
]}
/>
);
}
@@ -1,5 +1,4 @@
import { getTeamBySlug } from "@/api/team";
import { AppFooter } from "@/components/blocks/app-footer";
import { redirect } from "next/navigation";
import { isTeamOnboardingComplete } from "../../login/onboarding/isOnboardingRequired";
import { SaveLastVisitedTeamPage } from "../components/last-visited-page/SaveLastVisitedPage";
@@ -37,7 +36,6 @@ export default async function RootTeamLayout(props: {
{props.children}
</div>
<AppFooter />
<SaveLastVisitedTeamPage />
</div>
);
@@ -44,10 +44,16 @@ import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
import { upload } from "thirdweb/storage";
import { isZkSyncChain } from "thirdweb/utils";
import { FormHelperText, FormLabel, Text } from "tw-components";
import {
LAST_USED_PROJECT_ID,
LAST_USED_TEAM_ID,
} from "../../../app/team/[team_slug]/[project_slug]/components/SaveLastUsedProject";
import { useAddContractToProject } from "../../../app/team/[team_slug]/[project_slug]/hooks/project-contracts";
import { useCustomFactoryAbi, useFunctionParamsFromABI } from "../hooks";
import {
AddToProjectCardUI,
type MinimalProject,
type MinimalTeam,
type MinimalTeamsAndProjects,
} from "./add-to-project-card";
import { Fieldset } from "./common";
@@ -151,9 +157,42 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
const thirdwebClient = useThirdwebClient(jwt);
const [isImportEnabled, setIsImportEnabled] = useState(true);
const [importSelection, setImportSelection] = useState({
team: teamsAndProjects[0]?.team,
project: teamsAndProjects[0]?.projects[0],
const [importSelection, setImportSelection] = useState<{
team: MinimalTeam | undefined;
project: MinimalProject | undefined;
}>(() => {
const defaultSelection = {
team: teamsAndProjects[0]?.team,
project: teamsAndProjects[0]?.projects[0],
};
try {
const lastUsedTeamSlug = localStorage.getItem(LAST_USED_TEAM_ID);
const lastUsedProjectSlug = localStorage.getItem(LAST_USED_PROJECT_ID);
if (!lastUsedTeamSlug || !lastUsedProjectSlug) {
return defaultSelection;
}
const teamWithProjects = teamsAndProjects.find(
(t) => t.team.id === lastUsedTeamSlug,
);
const project = teamWithProjects?.projects.find(
(p) => p.id === lastUsedProjectSlug,
);
if (teamWithProjects && project) {
return {
team: teamWithProjects.team,
project,
};
}
} catch {
// ignore localStorage errors
}
return defaultSelection;
});
const activeAccount = useActiveAccount();
@@ -473,9 +512,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
name: params.contractMetadata?.name || "",
contractURI: _contractURI,
defaultAdmin: params.deployParams._defaultAdmin as string,
platformFeeBps: hasInbuiltDefaultFeeConfig
? DEFAULT_FEE_BPS_NEW
: DEFAULT_FEE_BPS,
platformFeeBps: DEFAULT_FEE_BPS_NEW,
platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
trustedForwarders: params.deployParams._trustedForwarders
? JSON.parse(params.deployParams._trustedForwarders as string)
@@ -10,6 +10,7 @@ import { cn } from "@/lib/utils";
import { CircleCheck, CircleIcon } from "lucide-react";
import Link from "next/link";
import { useCallback, useState } from "react";
import { useActiveWallet } from "thirdweb/react";
export type DeployModalStep = {
type: "deploy" | "setNFTMetadata";
@@ -79,9 +80,9 @@ export function DeployStatusModal(props: {
dialogCloseClassName="hidden"
className="gap-0 p-0 md:max-w-[480px]"
>
<div className="flex flex-col gap-6 p-6">
<div className="flex flex-col gap-5 p-6">
<DialogHeader>
<DialogTitle className="font-semibold text-2xl tracking-tight">
<DialogTitle className="font-semibold text-xl tracking-tight">
Deploy Status
</DialogTitle>
</DialogHeader>
@@ -125,7 +126,10 @@ type DeployModalStepProps = {
function RenderDeployModalStep(props: DeployModalStepProps) {
const { isActive, hasCompleted } = props;
const { title, description } = getStepInfo(props.step);
const wallet = useActiveWallet();
const requiresSignature = wallet?.id !== "inApp";
const { title, description } = getStepInfo(props.step, requiresSignature);
return (
<div className="rounded-lg border border-border bg-card">
<div
@@ -158,13 +162,16 @@ type TitleAndDesc = {
description: React.ReactNode;
};
function getStepInfo(step: DeployModalStep): TitleAndDesc {
function getStepInfo(
step: DeployModalStep,
requiresSignature: boolean,
): TitleAndDesc {
switch (step.type) {
case "deploy": {
return {
title: "Deploying contract",
description:
step.signatureCount > 0
step.signatureCount > 0 && requiresSignature
? `Your wallet will prompt you to sign ${
step.signatureCount === 1 ? "the" : step.signatureCount || 1
} transaction${step.signatureCount > 1 ? "s" : ""}.`
@@ -177,7 +184,7 @@ function getStepInfo(step: DeployModalStep): TitleAndDesc {
title: "Setting NFT metadata",
description: (
<>
{step.signatureCount > 0
{step.signatureCount > 0 && requiresSignature
? "Your wallet will prompt you to sign the transaction. "
: "This may take a few seconds."}
</>
@@ -1,27 +1,19 @@
"use client";
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { useLocalStorage } from "hooks/useLocalStorage";
import { CircleAlertIcon, XIcon } from "lucide-react";
import { useSelectedLayoutSegment } from "next/navigation";
import { XIcon } from "lucide-react";
export function AnnouncementBanner(props: {
function AnnouncementBannerUI(props: {
href: string;
label: string;
trackingLabel: string;
}) {
const layoutSegment = useSelectedLayoutSegment();
const [hasDismissedAnnouncement, setHasDismissedAnnouncement] =
useLocalStorage(`dismissed-${props.trackingLabel}`, false, true);
if (
layoutSegment === "/_not-found" ||
hasDismissedAnnouncement ||
layoutSegment === "login" ||
layoutSegment === "nebula-app" ||
layoutSegment === "join" ||
layoutSegment === "get-started"
) {
if (hasDismissedAnnouncement) {
return null;
}
@@ -34,7 +26,6 @@ export function AnnouncementBanner(props: {
target={props.href.startsWith("http") ? "_blank" : undefined}
className="ontainer flex cursor-pointer items-center gap-2 lg:justify-center "
>
<CircleAlertIcon className="hidden size-5 shrink-0 lg:block" />
<span className="inline-block font-medium leading-normal hover:underline">
{props.label}
</span>
@@ -53,12 +44,12 @@ export function AnnouncementBanner(props: {
);
}
export function OrganizeContractsToProjectsBanner() {
export function AnnouncementBanner() {
return (
<AnnouncementBanner
href="https://blog.thirdweb.com/organize-contracts/"
label="Your contracts are now organized into projects. Learn more"
trackingLabel="organize-contracts-to-projects"
<AnnouncementBannerUI
href="https://playground.thirdweb.com/connect/pay"
label="Let users pay with whatever they have without leaving your app"
trackingLabel="ub-launch"
/>
);
}
@@ -125,7 +125,7 @@ export const PayConfig: React.FC<PayConfigProps> = (props) => {
<Form {...form}>
<form onSubmit={handleSubmit} autoComplete="off">
<SettingsCard
bottomText="Shared Fees will are sent to recipient address"
bottomText="Shared Fees are sent to recipient address"
errorText={form.getFieldState("payoutAddress").error?.message}
saveButton={{
type: "submit",
@@ -139,9 +139,8 @@ export const PayConfig: React.FC<PayConfigProps> = (props) => {
Fee Sharing
</h3>
<p className="mt-1.5 mb-4 text-foreground text-sm">
thirdweb collects a 1% fee per end user transaction through{" "}
<strong>Buy With Crypto</strong>. We share 70% of this fee with
you.{" "}
thirdweb collects a 0.3% protocol fee on swap transactions. You
may set your own developer fee in addition to this fee.
<Link
href="https://portal.thirdweb.com/connect/pay/fee-sharing"
target="_blank"
@@ -41,7 +41,10 @@ import { z } from "zod";
import { projectDomainsSchema, projectNameSchema } from "../validations";
const ALL_PROJECT_SERVICES = SERVICES.filter(
(srv) => srv.name !== "relayer" && srv.name !== "chainsaw",
(srv) =>
srv.name !== "relayer" &&
srv.name !== "chainsaw" &&
srv.name !== "engineCloud", // TODO enable once API server is out
);
export type CreateProjectPrefillOptions = {
@@ -125,4 +125,4 @@ export type ApiKeyPayConfigValidationSchema = z.infer<
>;
// FIXME: Remove
export const HIDDEN_SERVICES = ["relayer", "chainsaw"];
export const HIDDEN_SERVICES = ["relayer", "chainsaw", "engineCloud"];
@@ -112,24 +112,31 @@ export function AccountAbstractionSettingsPage(
const policy = props.bundlerService;
const transformedQueryData = useMemo(
() => ({
const transformedQueryData = useMemo(() => {
const allowedContractAddresses = policy.allowedContractAddresses?.filter(
(x) => x !== "",
);
const allowedWallets = policy.allowedWallets?.filter((x) => x !== "");
const blockedWallets = policy.blockedWallets?.filter((x) => x !== "");
// there is a bug in API server that makes `allowedChainIds` an array with `0` if we set it to null
const allowedChainIds = policy.allowedChainIds?.filter((x) => x !== 0);
return {
allowedChainIds:
policy.allowedChainIds && policy.allowedChainIds?.length > 0
? policy.allowedChainIds
: null,
allowedChainIds && allowedChainIds?.length > 0 ? allowedChainIds : null,
allowedContractAddresses:
policy.allowedContractAddresses &&
policy.allowedContractAddresses?.length > 0
? joinWithComma(policy.allowedContractAddresses)
allowedContractAddresses && allowedContractAddresses.length > 0
? joinWithComma(allowedContractAddresses)
: null,
allowedWallets:
policy.allowedWallets && policy.allowedWallets?.length > 0
? joinWithComma(policy.allowedWallets)
allowedWallets && allowedWallets?.length > 0
? joinWithComma(allowedWallets)
: null,
blockedWallets:
policy.blockedWallets && policy.blockedWallets?.length > 0
? joinWithComma(policy.blockedWallets)
blockedWallets && blockedWallets?.length > 0
? joinWithComma(blockedWallets)
: null,
bypassWallets:
policy.bypassWallets && policy.bypassWallets?.length > 0
@@ -148,14 +155,13 @@ export function AccountAbstractionSettingsPage(
},
globalLimit: policy.limits?.global ?? null,
allowedOrBlockedWallets:
policy.allowedWallets && policy.allowedWallets?.length > 0
allowedWallets && allowedWallets?.length > 0
? "allowed"
: policy.blockedWallets && policy.blockedWallets?.length > 0
: blockedWallets && blockedWallets?.length > 0
? "blocked"
: null,
}),
[policy],
);
};
}, [policy]);
const form = useForm<z.infer<typeof aaSettingsFormSchema>>({
resolver: zodResolver(aaSettingsFormSchema),
@@ -212,11 +218,12 @@ export function AccountAbstractionSettingsPage(
const parsedValues: Omit<ProjectBundlerService, "name" | "actions"> =
{
allowedContractAddresses:
values.allowedContractAddresses !== null
? toArrFromList(values.allowedContractAddresses)
: null,
allowedChainIds: values.allowedChainIds,
allowedContractAddresses: values.allowedContractAddresses
? toArrFromList(values.allowedContractAddresses)
: null,
// don't set null - `updateProject` API adds chainId 0 to the list if its null and makes it `[0]`
allowedChainIds: values.allowedChainIds || [],
allowedWallets:
values.allowedOrBlockedWallets === "allowed" &&
values.allowedWallets !== null
@@ -41,6 +41,16 @@
/* Others */
--radius: 0.5rem;
/* Sidebar */
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 0 0% 4%;
--sidebar-primary: 221 83% 54%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 0 0% 93%;
--sidebar-accent-foreground: 0 0% 9%;
--sidebar-border: 0 0% 85%;
--sidebar-ring: 0 0% 80%;
}
.dark,
@@ -78,6 +88,16 @@
--active-border: 0 0% 22%;
--ring: 0 0% 30%;
--input: 0 0% 15%;
/* sidebar */
--sidebar-background: 0 0% 0%;
--sidebar-foreground: 0 0% 98%;
--sidebar-primary: 221 83% 54%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 0 0% 11%;
--sidebar-accent-foreground: 0 0% 98%;
--sidebar-border: 0 0% 15%;
--sidebar-ring: 0 0% 30%;
}
}
@@ -1,226 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import type { WalletNFT } from "./types";
/**
* Return the chain slug if the chain is supported by Simplehash, otherwise `undefined`
* Chain slug because we need it to fetch the owned NFTs using another endpoint
*/
export async function isSimpleHashSupported(
chainId: number,
): Promise<string | undefined> {
if (!process.env.SIMPLEHASH_API_KEY) {
throw new Error("No Simplehash API Key");
}
const options = {
method: "GET",
headers: {
"X-API-KEY": process.env.SIMPLEHASH_API_KEY,
},
};
const response: Array<{
chain: string;
eip155_network_id: number;
is_testnet: boolean;
}> = await fetch("https://api.simplehash.com/api/v0/chains", options)
.then((r) => r.json())
.catch(() => []);
const found = response.find((chain) => chain.eip155_network_id === chainId);
return found?.chain;
}
export function useSimplehashSupport(chainId: number) {
return useQuery({
queryKey: ["simplehash-supported", chainId],
queryFn: () => isSimpleHashSupported(chainId),
enabled: !!chainId,
});
}
export function generateSimpleHashUrl({
chainSlug,
owner,
}: { chainSlug: string; owner: string }) {
const url = new URL("https://api.simplehash.com/api/v0/nfts/owners");
url.searchParams.append("wallet_addresses", owner);
url.searchParams.append("chains", chainSlug);
return url.toString();
}
export async function transformSimpleHashResponseToNFT(
simpleHashResponse: SimpleHashResponse,
walletAddress: string,
): Promise<WalletNFT[]> {
return (
await Promise.all(
simpleHashResponse.nfts.map(async (simpleHashNft) => {
try {
return {
id: simpleHashNft.token_id,
contractAddress: simpleHashNft.contract_address,
tokenId: simpleHashNft.token_id,
metadata: {
name: simpleHashNft.name,
description: simpleHashNft.description,
image: simpleHashNft.extra_metadata.image_original_url,
external_url: simpleHashNft.external_url,
attributes: simpleHashNft.extra_metadata.attributes,
properties: simpleHashNft.extra_metadata.properties,
id: simpleHashNft.token_id,
uri: simpleHashNft.extra_metadata.metadata_original_url,
animation_url:
simpleHashNft.extra_metadata.animation_original_url,
background_color: simpleHashNft.background_color,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
} as any,
owner: walletAddress,
supply: simpleHashNft.token_count.toString(),
type: simpleHashNft.contract.type,
tokenURI: simpleHashNft.extra_metadata.metadata_original_url,
} as WalletNFT;
} catch {
return undefined as unknown as WalletNFT;
}
}),
)
).filter(Boolean);
}
interface PreviewImages {
image_small_url: string;
image_medium_url: string;
image_large_url: string;
image_opengraph_url: string;
blurhash: string;
predominant_color: string;
}
interface ImageProperties {
width: number;
height: number;
size: number;
mime_type: string;
}
interface Owner {
owner_address: string;
quantity: number;
first_acquired_date: string;
last_acquired_date: string;
}
interface Contract {
type: string;
name: string;
symbol: string;
deployed_by: string;
deployed_via_contract: string;
}
interface MarketplacePage {
marketplace_id: string;
marketplace_name: string;
marketplace_collection_id: string;
nft_url: string;
collection_url: string;
verified: boolean;
}
interface Collection {
collection_id: string;
name: string;
description: string;
image_url: string;
banner_image_url: string | null;
category: string | null;
is_nsfw: boolean | null;
external_url: string | null;
twitter_username: string | null;
discord_url: string | null;
instagram_username: string | null;
medium_username: string | null;
telegram_url: string | null;
marketplace_pages: MarketplacePage[];
metaplex_mint: string | null;
metaplex_first_verified_creator: string | null;
floor_prices: unknown[];
top_bids: unknown[];
distinct_owner_count: number;
distinct_nft_count: number;
total_quantity: number;
chains: string[];
top_contracts: string[];
}
interface FirstCreated {
minted_to: string;
quantity: number;
timestamp: string;
block_number: number;
transaction: string;
transaction_initiator: string;
}
interface Rarity {
rank: number;
score: number;
unique_attributes: number;
}
interface ExtraMetadataAttribute {
trait_type: string;
value: string;
display_type: string | null;
}
interface ExtraMetadataProperties {
number: number;
name: string;
}
interface ExtraMetadata {
attributes: ExtraMetadataAttribute[];
properties: ExtraMetadataProperties;
image_original_url: string;
animation_original_url: string | null;
metadata_original_url: string;
}
interface NFT {
nft_id: string;
chain: string;
contract_address: string;
token_id: string;
name: string;
description: string;
previews: PreviewImages;
image_url: string;
image_properties: ImageProperties;
video_url: string | null;
video_properties: unknown;
audio_url: string | null;
audio_properties: unknown;
model_url: string | null;
model_properties: unknown;
other_url: string | null;
other_properties: unknown;
background_color: string | null;
external_url: string | null;
created_date: string;
status: string;
token_count: number;
owner_count: number;
owners: Owner[];
contract: Contract;
collection: Collection;
last_sale: unknown;
first_created: FirstCreated;
rarity: Rarity;
royalty: unknown[];
extra_metadata: ExtraMetadata;
}
interface SimpleHashResponse {
nfts: NFT[];
}
+1 -1
View File
@@ -27,7 +27,7 @@ import {
} from "thirdweb/chains";
// Cannot use BigInt for the values here because it will result in error: "fail to serialize bigint"
// when the data is being sent from server to client (when we fetch the owned NFTs from simplehash/alchemy/moralis)
// when the data is being sent from server to client (when we fetch the owned NFTs from insight/alchemy/moralis)
export type WalletNFT = Omit<NFT, "id" | "supply"> & {
id: string;
contractAddress: string;
+10
View File
@@ -71,6 +71,16 @@ module.exports = {
link: {
foreground: "hsl(var(--link-foreground))",
},
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
},
borderRadius: {
lg: "var(--radius)",
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

@@ -0,0 +1,21 @@
import type React from "react";
import { APIHeader } from "../../../../components/blocks/APIHeader";
export default function Layout(props: {
children: React.ReactNode;
}) {
return (
<div>
<APIHeader
title="Universal Bridge API"
description={
<>HTTP API to bridge, swap and onramp to and from any currency</>
}
docsLink="https://portal.thirdweb.com/connect/pay/overview"
heroLink="/ub.png"
/>
{props.children}
</div>
);
}
@@ -0,0 +1,79 @@
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import Link from "next/link";
import { getBridgePaths } from "./utils";
export default async function Page() {
try {
const paths = await getBridgePaths();
return (
<div className="pb-20">
<h2 className="mb-2 font-semibold text-2xl tracking-tight">
Universal Bridge REST API
</h2>
<p className="mb-5 text-muted-foreground">
Directly interact with the Universal Bridge API from your backend,
using standard REST api.
</p>
<div className="flex flex-col gap-8">
<BlueprintSection
title="Available endpoints"
blueprints={paths.map(([pathName, pathObj]) => {
if (!pathObj) {
throw new Error(`Path not found: ${pathName}`);
}
return {
name: pathName,
description: pathObj.get?.description || "",
link: `/connect/pay/backend/reference?route=${pathName}`,
};
})}
/>
</div>
</div>
);
} catch (error) {
console.error(error);
return <div>Error fetching API spec</div>;
}
}
function BlueprintSection(props: {
title: string;
blueprints: { name: string; description: string; link: string }[];
}) {
return (
<div className="overflow-hidden rounded-lg border bg-card">
<div className="flex items-center gap-2 border-b bg-accent/20 px-6 py-4">
<h2 className="font-semibold text-lg tracking-tight">{props.title}</h2>
</div>
<Table>
<TableBody>
{props.blueprints.map((item) => (
<TableRow
key={item.link}
className="group hover:bg-accent/50"
linkBox
>
<TableCell>
<span className="flex items-center gap-3">
<Link
href={item.link}
className="before:absolute before:inset-0"
>
<div className="flex flex-col">
<p className="font-semibold text-md">{item.name}</p>
<p className="text-muted-foreground text-sm">
{item.description}
</p>
</div>
</Link>
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
@@ -0,0 +1,70 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { redirect } from "next/navigation";
import { THIRDWEB_CLIENT } from "../../../../../lib/client";
import { isProd } from "../../../../../lib/env";
import { BlueprintPlayground } from "../../../../insight/[blueprint_slug]/blueprint-playground.client";
import { getBridgePaths } from "../utils";
export default async function Page(props: {
searchParams: Promise<{
route: string;
}>;
}) {
const params = await props.searchParams;
// invalid url
if (!params.route) {
redirect("/connect/pay/backend");
}
const thirdwebDomain = !isProd ? "thirdweb-dev" : "thirdweb";
const domain = `https://bridge.${thirdwebDomain}.com`;
const paths = await getBridgePaths();
const pathMetadata = paths.find(([path]) => path === params.route)?.[1]?.get;
// invalid url
if (!pathMetadata) {
redirect("/connect/pay/backend");
}
const title = pathMetadata.summary || "";
return (
<div>
<Breadcrumbs />
<h1 className="mt-3 mb-6 font-semibold text-2xl tracking-tight lg:text-3xl">
{title}
</h1>
<BlueprintPlayground
key={params.route}
metadata={pathMetadata}
backLink={"/connect/pay/backend"}
clientId={THIRDWEB_CLIENT.clientId}
path={params.route}
supportedChainIds={[]}
domain={domain}
/>
</div>
);
}
function Breadcrumbs() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/connect/pay/backend">
Universal Bridge API
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
</BreadcrumbList>
</Breadcrumb>
);
}
@@ -0,0 +1,13 @@
import type { OpenAPIV3 } from "openapi-types";
import { isProd } from "../../../../lib/env";
export async function getBridgePaths() {
const thirdwebDomain = !isProd ? "thirdweb-dev" : "thirdweb";
const res = await fetch(`https://bridge.${thirdwebDomain}.com/openapi.json`);
const openapiJson = (await res.json()) as OpenAPIV3.Document;
return Object.entries(openapiJson.paths).filter(
([, pathObj]) =>
pathObj?.get?.deprecated === undefined ||
pathObj?.get?.deprecated === false,
);
}
@@ -44,7 +44,7 @@ function BuyMerch() {
Commerce
</h2>
<p className="max-w-[600px]">
Take paymets from Fiat or Crypto directly to your seller wallet.
Take payments from Fiat or Crypto directly to your seller wallet.
<br />
Get notified for every sale through webhooks, which lets you trigger
any action you want like shipping physical goods, activating services
@@ -105,8 +105,8 @@ export function LeftSection(props: {
<CustomRadioGroup
id="pay-mode"
options={[
{ value: "fund_wallet", label: "Fund Wallet" },
{ value: "direct_payment", label: "Direct Payment" },
{ value: "fund_wallet", label: "Buy Crypto" },
{ value: "direct_payment", label: "Purchase" },
{ value: "transaction", label: "Transaction" },
]}
onValueChange={(value) => {
@@ -74,7 +74,7 @@ export function RightSection(props: {
? "Transaction"
: props.options.payOptions.mode === "direct_payment"
? "Purchase"
: "Fund Wallet"),
: "Buy Crypto"),
image:
props.options.payOptions.image ||
`https://placehold.co/600x400/${
@@ -7,7 +7,7 @@ import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase,
title: "Fund wallets | thirdweb Universal Bridge",
title: "Buy Crypto | thirdweb Universal Bridge",
description:
"The easiest way for users to fund their wallets. Onramp users in clicks and generate revenue for each user transaction. Integrate for free.",
};
@@ -41,7 +41,7 @@ function StyledPayEmbed() {
<>
<div className="space-y-2">
<h2 className="font-semibold text-2xl tracking-tight sm:text-3xl">
Fund Wallet
Buy Crypto
</h2>
<p className="max-w-[600px]">
Inline component that allows users to buy any currency.
@@ -9,7 +9,7 @@ export const metadata: Metadata = {
metadataBase,
title: "Sign In, Account Abstraction and SIWE Auth | thirdweb ConnectEmbed",
description:
"Let users sign up with their email, phone number, social media accounts or directly with a wallet. Seemlessly integrate account abstraction and SIWE auth.",
"Let users sign up with their email, phone number, social media accounts or directly with a wallet. Seamlessly integrate account abstraction and SIWE auth.",
};
export default function Page() {
@@ -10,7 +10,7 @@ export const metadata: Metadata = {
metadataBase,
title: "Sign In, Account Abstraction and SIWE Auth | thirdweb Connect",
description:
"Let users sign up with their email, phone number, social media accounts or directly with a wallet. Seemlessly integrate account abstraction and SIWE auth.",
"Let users sign up with their email, phone number, social media accounts or directly with a wallet. Seamlessly integrate account abstraction and SIWE auth.",
};
export default function Page() {
@@ -35,7 +35,6 @@ import {
useForm,
} from "react-hook-form";
import { z } from "zod";
import { isProd } from "../../../lib/env";
import type { BlueprintParameter, BlueprintPathMetadata } from "../utils";
export function BlueprintPlayground(props: {
@@ -44,6 +43,7 @@ export function BlueprintPlayground(props: {
clientId: string;
path: string;
supportedChainIds: number[];
domain: string;
}) {
const [abortController, setAbortController] =
useState<AbortController | null>(null);
@@ -56,6 +56,9 @@ export function BlueprintPlayground(props: {
try {
const res = await fetch(url, {
signal: controller.signal,
headers: {
"x-client-id": props.clientId,
},
});
return {
status: res.status,
@@ -78,8 +81,6 @@ export function BlueprintPlayground(props: {
},
});
const thirdwebDomain = !isProd ? "thirdweb-dev" : "thirdweb";
return (
<BlueprintPlaygroundUI
backLink={props.backLink}
@@ -98,7 +99,7 @@ export function BlueprintPlayground(props: {
abortController.abort();
}
}}
domain={`https://insight.${thirdwebDomain}.com`}
domain={props.domain}
path={props.path}
supportedChainIds={props.supportedChainIds}
/>
@@ -7,6 +7,7 @@ import {
} from "@/components/ui/breadcrumb";
import { redirect } from "next/navigation";
import { THIRDWEB_CLIENT } from "../../../lib/client";
import { isProd } from "../../../lib/env";
import { fetchBlueprintSpec } from "../utils";
import { BlueprintPlayground } from "./blueprint-playground.client";
@@ -26,6 +27,9 @@ export default async function Page(props: {
redirect("/insight");
}
const thirdwebDomain = !isProd ? "thirdweb-dev" : "thirdweb";
const domain = `https://insight.${thirdwebDomain}.com`;
const [blueprintSpec] = await Promise.all([
fetchBlueprintSpec({
blueprintId: params.blueprint_slug,
@@ -58,6 +62,7 @@ export default async function Page(props: {
clientId={THIRDWEB_CLIENT.clientId}
path={searchParams.path}
supportedChainIds={supportedChainIds}
domain={domain}
/>
</div>
);
+30 -23
View File
@@ -60,28 +60,6 @@ export const staticSidebarLinks: SidebarLink[] = [
},
],
},
{
name: "Universal Bridge",
expanded: false,
links: [
{
name: "UI Component",
href: "/connect/pay",
},
{
name: "Fund Wallet",
href: "/connect/pay/fund-wallet",
},
{
name: "Commerce",
href: "/connect/pay/commerce",
},
{
name: "Transactions",
href: "/connect/pay/transactions",
},
],
},
{
name: "Auth",
href: "/connect/auth",
@@ -124,6 +102,34 @@ export const staticSidebarLinks: SidebarLink[] = [
},
];
const universalBridgeSidebarLinks: SidebarLink = {
name: "Universal Bridge",
isCollapsible: false,
expanded: false,
links: [
{
name: "UI Component",
href: "/connect/pay",
},
{
name: "Buy Crypto",
href: "/connect/pay/fund-wallet",
},
{
name: "Commerce",
href: "/connect/pay/commerce",
},
{
name: "Transactions",
href: "/connect/pay/transactions",
},
{
name: "Backend API",
href: "/connect/pay/backend",
},
],
};
const engineSidebarLinks: SidebarLink = {
name: "Engine",
isCollapsible: false,
@@ -167,13 +173,14 @@ export async function getSidebarLinks() {
const sidebarLinks: SidebarLink[] = [
...staticSidebarLinks,
universalBridgeSidebarLinks,
engineSidebarLinks,
{
name: "Insight",
isCollapsible: false,
expanded: false,
links: insightLinks,
},
engineSidebarLinks,
];
return sidebarLinks;
@@ -15,7 +15,7 @@ export function BuyMerchPreview() {
payOptions={{
mode: "direct_payment",
paymentInfo: {
amount: "2",
amount: "0.25",
chain: base,
token: getDefaultToken(base, "USDC"),
sellerAddress: "0xEb0effdFB4dC5b3d5d3aC6ce29F3ED213E95d675",
+1
View File
@@ -19,6 +19,7 @@ export const THIRDWEB_CLIENT = createThirdwebClient(
process.env.THIRDWEB_SECRET_KEY
? {
secretKey: process.env.THIRDWEB_SECRET_KEY,
clientId: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID as string,
config: {
storage: isDev
? {
@@ -155,6 +155,8 @@ function extractPageLLMContent(
const htmlToMarkdown = new NodeHtmlMarkdown({
keepDataImages: false,
ignore: ["button"],
maxConsecutiveNewlines: 2,
});
let linksContent = "";
@@ -167,10 +169,7 @@ function extractPageLLMContent(
let description = "";
for (const p of paragraphs) {
// skip noindex or no-llm paragraphs
if (
p.getAttribute("data-noindex") === "true" ||
p.getAttribute("data-no-llm") === "true"
) {
if (p.closest("[data-noindex]") || p.closest("[data-no-llm]")) {
continue;
}
@@ -180,7 +179,7 @@ function extractPageLLMContent(
}
}
linksContent += `* [${pageTitle}](${pageUrl}): ${description}`;
linksContent += `* [${pageTitle}](${pageUrl}): ${description || `Reference for ${pageTitle}`}`;
// Remove noindex and no-llm elements
const contentElements = main.querySelectorAll("*");
@@ -204,12 +203,18 @@ function extractPageLLMContent(
// prefix all the relative links with the `https://portal.thirdweb.com`
const links = main.querySelectorAll("a");
for (const link of links) {
const [path, hash] = link.getAttribute("href")?.split("#") || [];
if (path?.startsWith("/")) {
link.setAttribute(
"href",
`https://portal.thirdweb.com${path}${hash ? `#${hash}` : ""}`,
);
const href = link.getAttribute("href");
if (href?.startsWith("/")) {
link.setAttribute("href", `https://portal.thirdweb.com${href}`);
}
}
// prefix all relative image links with the `https://portal.thirdweb.com`
const images = main.querySelectorAll("img");
for (const image of images) {
const src = image.getAttribute("src");
if (src?.startsWith("/")) {
image.setAttribute("src", `https://portal.thirdweb.com${src}`);
}
}
@@ -12,7 +12,7 @@ export const metadata = createMetadata({
description: "How to batch transactions with smart accounts",
});
Batching transactions allows sending multiple transactions in a single user operation. This can be useful to save on fees, reduce number of user confimations or to ensure that multiple transactions are executed atomically.
Batching transactions allows sending multiple transactions in a single user operation. This can be useful to save on fees, reduce number of user confirmations or to ensure that multiple transactions are executed atomically.
A typical example is to do an approval and a transfer in a single userOperation. This way, the transfer will only happen if the approval is successful.
+1 -1
View File
@@ -83,7 +83,7 @@ Get started with Connect in your preferred language.
- **Connect to 500+ different wallet providers** with support for every EVM network
- **Log in and authenticate your users** with customizable and secure [email, phone, passkeys and social login](https://portal.thirdweb.com/connect/in-app-wallet/overview) flows.
- **Sponsor transactions** to seamlessly onboard non-native or new crypto users with [Account Abstraction](https://portal.thirdweb.com/connect/account-abstraction/overview).
- **Perform wallet actions** like connecting and disconnecting wallets, viewing balance, displaying ENS names, and execute transactions with our [perfomant, reliable and type safe API](https://portal.thirdweb.com/connect/blockchain-api)
- **Perform wallet actions** like connecting and disconnecting wallets, viewing balance, displaying ENS names, and execute transactions with our [performant, reliable and type safe API](https://portal.thirdweb.com/connect/blockchain-api)
- **Easily integrate with thirdweb's contracts** to enable users to interact with your application.
- **Facilitate payments** by letting user top up their wallets or do onchain purchases with a credit card with [Pay](https://portal.thirdweb.com/connect/pay/overview).
@@ -16,11 +16,7 @@ thirdweb does not charge any fees for buy with fiat. Any fees on onramping are b
</Details>
<Details summary="What are the fees for Buy With Crypto?">
For buying with crypto, thirdweb charges 0.3% and the application developer may charge up to 0.7% on each transaction
</Details>
<Details summary="How does fee sharing work for Buy With Crypto?">
thirdweb shares 70% of all fees collected through Buy With Crypto with our developers. Fee sharing is not currently available for Buy With Fiat transactions.
For buying with crypto or swap fees, thirdweb charges a 0.3% protocol fee. Developers may monetize their application by charging additional fees set through the dashboard.
</Details>
<Details summary="What does the term “network fees” refer to?">
@@ -1,23 +0,0 @@
import { createMetadata, DocImage } from "@doc";
import paySettingsImage from "../assets/pay-settings.png";
export const metadata = createMetadata({
image: {
title: "thirdweb Universal Bridge - Fee Sharing",
icon: "thirdweb",
},
title: "thirdweb Universal Bridge - Fee Sharing | thirdweb",
description: "How to participate in fee sharing with thirdweb Universal Bridge",
});
# Fee Sharing
thirdweb collects a 1% fee per end user transaction. We share 70% of this fee with you.
For example, if a user purchases $100 worth of Polygon through your application, thirdweb charges a total of $1.00. thirdweb collects $0.30 and sends $0.70 to you.
Plase note that fee sharing only applies to fees collected through swaps and bridges. Fiat purchases do not currently qualify for fee sharing.
To participate in fee sharing, all you need to do is set a recipient address in your dashboard. Your recipient address can be configured in your dashboard by navigating to your Team > Project > Connect > Universal Bridge in [thirdweb dashboard](https://www.thirdweb.com/team).
<DocImage src={paySettingsImage} />
@@ -36,7 +36,7 @@ In this guide, we'll show you how to purchase 0.01 Base ETH from USD in Typescri
<Step title='Install the Connect SDK'>
<InstallTabs
npm="npm i thirdweb"
yarn="yarn install thirdweb"
yarn="yarn add thirdweb"
pnpm="pnpm i thirdweb"
/>
<Step title='Get Your Client ID'>
@@ -0,0 +1,214 @@
import {
createMetadata,
Callout,
DocImage,
InstallTabs,
Steps,
Step,
} from "@doc";
import OnrampStepOne from "../../assets/avax-to-usd.png";
export const metadata = createMetadata({
image: {
title: "thirdweb Universal Bridge - Cross-Chain Swapping",
icon: "thirdweb",
},
title: "thirdweb Universal Bridge - Cross-Chain Swapping | thirdweb",
description:
"Learn how to build a custom cross-chain swapping experience with thirdweb Universal Bridge.",
});
# Leverage Cross-Chain Swaps with Universal Bridge
Learn how to enable your users to swap from any asset to any other with thirdweb's Universal Bridge.
In this guide, we'll show you how to purchase 10 USDC on Optimism in Typescript.
---
<Steps>
<Step title='Install the Connect SDK'>
<InstallTabs
npm="npm i thirdweb"
yarn="yarn add thirdweb"
pnpm="pnpm i thirdweb"
/>
<Step title='Get Your Client ID'>
Log in to the [thirdweb dashboard](https://thirdweb.com/team). Click on Create New > Project to get your **Client ID**. You'll need your Client ID to interact with the Connect SDK.
</Step>
</Step>
<Step title='Find available routes'>
Before your user can select which tokens they'd like to swap, they'll need to find available routes.
You can do this using the `routes` function in our `Bridge` namespace. You or your users can filter by origin and destination chains and/or tokens. Pagination is also built-in to the function.
```tsx
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
// Get all available routes
const allRoutes = await Bridge.routes({
client: thirdwebClient,
});
// Filter routes for a specific token or chain
const filteredRoutes = await Bridge.routes({
originChainId: 1, // From Ethereum
originTokenAddress: NATIVE_TOKEN_ADDRESS,
destinationChainId: 10, // To Optimism
client: thirdwebClient,
});
// Paginate through routes
const paginatedRoutes = await Bridge.routes({
limit: 10,
offset: 0,
client: thirdwebClient,
});
```
This will return an array of `Route` objects, which will include information such as `symbol`, `address`, and `chainId` for both the origin and destination tokens.
</Step>
<Step title='Get a quote'>
Once you know which routes are available, you can retrieve a quote to show the user how much they can expect to pay for a given swap.
In this example, we'll use the `Buy.quote` function to get a quote for buying 10 USDC on Optimism for Base ETH.
<Callout variant="info">
The `Buy` namespace is purpose-built for when you want to obtain a specific amount of the output token.
If you have a specific input amount and are flexible on the output amount, you can use the `Sell` namespace.
Learn more about sells [here](https://portal.thirdweb.com/references/typescript/v5/sell/prepare).
</Callout>
Quote allows us to get an expected amount before the user has connected their wallet. This quote won't come with executable transactions, and won't be a guaranteed price.
```tsx
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
const buyQuote = await Bridge.Buy.quote({
originChainId: 8453, // Base
originTokenAddress: NATIVE_TOKEN_ADDRESS,
destinationChainId: 10, // Optimism
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism USDC
buyAmountWei: 10000000n, // 10 USDC
client: thirdwebClient,
});
console.log(
`To get ${buyQuote.destinationAmount} wei on destination chain, you need to pay ${buyQuote.originAmount} wei`,
);
```
This will return a `Quote` object, which will include the `originAmount` and `destinationAmount` in wei, along with some more useful information about the predicted quote.
</Step>
<Step title='Get the prepared transaction'>
Now that we know how much the user can expect to pay, we can have them connect their wallet and execute the swap.
To get a prepared quote, we'll use the `Buy.prepare` function. The key difference with this function is it requires a `sender` and `receiver` to be specified.
```tsx
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
const preparedBuy = await Bridge.Buy.prepare({
originChainId: 8453, // Base
originTokenAddress: NATIVE_TOKEN_ADDRESS,
destinationChainId: 10, // Optimism
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism USDC
buyAmountWei: 10000000n, // 10 USDC
sender: "0x...", // Your user's wallet address
receiver: "0x...", // Recipient address (can be the same as sender)
client: thirdwebClient,
});
// The prepared quote contains the transactions you need to execute
console.log(`Transactions to execute: ${preparedBuy.transactions.length}`);
```
This will return a `PreparedQuote` object. It will look very similar to the `Quote` you received in the previous step, but it will include a `transactions` array. This array will contain the transactions you need to execute to complete the swap.
</Step>
<Step title='Execute the swap'>
To execute the swap, we'll need to send all transactions in the `transactions` array one after the other.
<Callout variant="warning">
Currently, the `transactions` array does not include approvals. You'll need to execute any necessary approvals prior to executing the swap transactions.
</Callout>
```tsx
import { sendTransaction, waitForReceipt } from "thirdweb";
const preparedBuy = await Bridge.Buy.prepare({
originChainId: 8453, // Base
originTokenAddress: NATIVE_TOKEN_ADDRESS,
destinationChainId: 10, // Optimism
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism USDC
buyAmountWei: 10000000n, // 10 USDC
sender: "0x...", // Your user's wallet address
receiver: "0x...", // Recipient address (can be the same as sender)
client: thirdwebClient,
});
};
for (const transaction of preparedBuy.transactions) {
const tx = prepareTransaction({
to: transaction.to as string,
value: BigInt(transaction.value ?? 0n),
data: transaction.data,
chain: defineChain(transaction.chainId),
client
});
const result = await sendAndConfirmTransaction({ transaction: tx, account });
let swapStatus;
do {
swapStatus = await Bridge.status({
transactionHash: result.transactionHash,
client,
});
} while (swapStatus.status !== "COMPLETED");
};
```
<Callout variant="info">
The returned transactions follow the [Ox](https://oxlib.sh/) standard [TransactionEnvelopeEip1559](https://oxlib.sh/guides/transaction-envelopes)
format. This is a simple transaction with `data`, `to`, `value`, and `chainId` properties. **Gas is not included in the transaction, and should
be estimated separately if necessary.** Normally, the thirdweb SDK will handle this for you.
</Callout>
</Step>
<Step title='Getting the swap status'>
You'll notice in the previous step we call `Bridge.status` to get the status of the swap. We can get the status of any past swap using just the transaction hash and chain ID of its origin transaction.
When sending the transactions in a prepared quote, you **must** use `Bridge.status` to get the `COMPLETED` status before moving on to the next transaction. This is because `status` waits for both the origin
and destination transactions to complete. Waiting for the origin transaction receipt is not sufficient since the funds might not have arrived on the destination chain yet.
The `status` will also return all transactions (origin and destination) involved in the swap.
```tsx
import { Bridge } from "thirdweb";
// Check the status of a bridge transaction
const bridgeStatus = await Bridge.status({
transactionHash:
"0xe199ef82a0b6215221536e18ec512813c1aa10b4f5ed0d4dfdfcd703578da56d",
chainId: 8453, // The chain ID where the transaction was initiated
client: thirdwebClient,
});
// The status will be one of: "COMPLETED", "PENDING", "FAILED", or "NOT_FOUND"
if (bridgeStatus.status === "completed") {
console.log(`
Bridge completed!
Sent: ${bridgeStatus.originAmount} wei on chain ${bridgeStatus.originChainId}
Received: ${bridgeStatus.destinationAmount} wei on chain ${bridgeStatus.destinationChainId}
`);
} else if (bridgeStatus.status === "pending") {
console.log("Bridge transaction is still pending...");
} else {
console.log("Bridge transaction failed");
}
```
</Step>
</Steps>
@@ -1,6 +1,7 @@
import { createMetadata, DocImage, Grid, SDKCard, FeatureCard } from "@doc";
import PayOverviewImage from "../assets/pay-overview.png";
import SupportedChains from "../../../_images/supported-chains.png";
import {Rocket, ArrowLeftRight, Wallet, Pencil, ShieldCheck, PiggyBank, Globe, Component} from "lucide-react";
export const metadata = createMetadata({
image: {
@@ -14,7 +15,7 @@ export const metadata = createMetadata({
# Overview
Universal Bridge allows your users to bridge, swap, and purchase cryptocurrencies and execute transactions with any fiat options or tokens via cross-chain routing.
Universal Bridge allows your users to bridge, swap, and purchase cryptocurrencies and execute transactions with any fiat options or tokens via cross-chain routing. It enables users to purchase or complete in-app transactions with any token they hold.
<DocImage src={PayOverviewImage} />
@@ -24,46 +25,46 @@ Universal Bridge allows your users to bridge, swap, and purchase cryptocurrencie
className="my-4 grid gap-2 md:grid-cols-2 lg:grid-cols-2 "
>
<FeatureCard
title="Earn money"
description="Receive 70% of fees on each crypto-to-crypto transaction"
iconUrl="/icons/feature-cards/earn-money.svg"
title="Avoid chargeback & liability"
description="Automatic screening for sanctioned wallets and regions to ensure application safety."
iconUrl={<Rocket />}
/>
<FeatureCard
title="Onboard users"
description="Fiat on-ramps to enable purchases with traditional payment methods"
iconUrl="/icons/feature-cards/onboard-users.svg"
title="Global Coverage"
description="Bridging support in over 160+ countries to reach a truly global audience."
iconUrl={<Globe/>}
/>
<FeatureCard
title="Enable in-app purchases"
description="Purchase crypto without leaving the application"
iconUrl="/icons/feature-cards/in-app-purchase.svg"
title="Earn revenue"
description="Monetize your application and earn swap fees on each transaction."
iconUrl={<PiggyBank />}
/>
<FeatureCard
title="Chain Coverage"
description="Support purchases on over 20+ widely used EVM chains"
iconUrl="/icons/feature-cards/chain-coverage.svg"
title="Transact Securely"
description="Strict one-time approvals ensuring funds are not at risk of exploits."
iconUrl={<ShieldCheck />}
/>
<FeatureCard
title="Liquidity Aggregation"
description="Ensure every transaction is possible"
iconUrl="/icons/feature-cards/liquidity-aggregation.svg"
title="Easy-to-integrate Components"
description="Easily integrate components for buying funds, e-commerce checkouts, and more."
iconUrl={<Component />}
/>
<FeatureCard
title="Global Coverage"
description="Support for over 130+ countries"
iconUrl="/icons/feature-cards/global-coverage.svg"
title="Customizable Flows"
description="Create your own customized headless flows using the Universal Bridge SDK or API."
iconUrl={<Pencil/>}
/>
<FeatureCard
title="Integration Options"
description="Use pre-built modals or customize the transaction experience"
iconUrl="/icons/feature-cards/integration-options.svg"
title="Flexible Wallet Payments"
description="Enable payments with any wallet that has funds not just the one connected."
iconUrl= {<Wallet />}
/>
<FeatureCard
title="Secure"
description="Strict one-time approvals mean user funds are not at risk of smart contract allowance exploits"
iconUrl="/icons/feature-cards/security.svg"
title="Multi-hop Routing"
description="Handle multiple swaps at once within one session for a seamless swap experience."
iconUrl={<ArrowLeftRight />}
/>
</div>
@@ -74,30 +75,20 @@ Universal Bridge is supported on select EVM compatible chains. To view the full
<DocImage src={SupportedChains} />
## Fee Sharing
Thirdweb collects a 1% fee per end-user transaction and shares 70% of that fee with you. For example, on a $100 Polygon purchase, a $1.00 fee is charged—of which $0.30 is retained by thirdweb and $0.70 is remitted to you.
Note: Fee sharing applies only to fees from swaps and bridges; fiat purchases are excluded.
To enable fee sharing, simply set a recipient address in your dashboard under Team > Project > Connect > Universal Bridge in the thirdweb dashboard.
## Onramp providers
## Onramp Providers
| Provider | Supported Countries |
| -------- | ------------------------------------------------------------------------------------------------------- |
| Transak | https://transak.notion.site/On-Ramp-Payment-Methods-Fees-Other-Details-b0761634feed4b338a69f4f186d906a5 |
| Kado | https://www.kado.money/supported-countries |
| Stripe | https://docs.stripe.com/crypto/onramp |
Specify a preferred onramp provider by setting the preferredProvider parameter in ConnectButton, PayEmbed, or Headless Integration. For details, refer to
[customization](../pay/customization/connectbutton#preferredProvider) or the [API Reference for preferredProvider](/references/typescript/v5/GetBuyWithFiatQuoteParams).
| Coinbase | https://docs.cdp.coinbase.com/onramp/docs/payment-methods/ |
## Integration Options
| OPTION | BEST FOR |
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [ConnectButton](/connect/pay/get-started#option-1-connectbutton) | Developers who want out-of-the-box support complete with wallet and social login. |
| [PayEmbed](/connect/pay/get-started#option-2-embed-pay) | Developers who want onramp and crypto purchase experiences directly in their application. |
| [Transaction Flow (sendTransaction)](/connect/pay/get-started#option-3-send-a-transaction-with-pay) | Developers who want users to onramp or purchase crypto directly into a transaction. Great for minting and NFT purchase flows. |
| [Custom Universal Bridge Experience](/connect/pay/build-a-custom-experience) | Developers who want full customization over every step of the onramp process, from displaying quotes to polling for transaction statuses. |
| [Buy Crypto](https://playground.thirdweb.com/connect/pay/) | Developers who want onramp and crypto purchase experiences directly in their application. |
| [Transactions](https://playground.thirdweb.com/connect/pay/transactions) | Developers who want users to onramp or purchase crypto directly into a transaction. Great for minting and NFT purchase flows. |
| [In-App Purchases](https://playground.thirdweb.com/connect/pay/commerce) | Developers who want to take payments from Fiat or Crypto directly to a seller wallet |
| [Headless](https://playground.thirdweb.com/connect/pay/backend) | Developers who prefer a headless customized flow using APIs. |
@@ -9,7 +9,7 @@ export const metadata = createMetadata({
# Webhooks
Pay can be configured to send webhook events to notify your application any time an event happens on your transaction. Pay sends a response, via a HTTP request, to any endpoint URLs that you have provided us in your Team > Project > Connect > Pay > Webhooks page in [thirdweb dashboard](https://thirdweb.com/team).
Universal Bridge can be configured to send webhook events to notify your application any time an event happens on your transaction. Universal Bridge sends a response, via a HTTP request, to any endpoint URLs that you have provided us in your Team > Project > Connect > Universal Bridge > Webhooks page in [thirdweb dashboard](https://thirdweb.com/team).
## Events
+5 -1
View File
@@ -314,9 +314,13 @@ export const sidebar: SideBar = {
href: `${paySlug}/guides/accept-direct-payments`,
},
{
name: "Build a Custom Experience",
name: "Build a Custom Onramp Experience",
href: `${paySlug}/guides/build-a-custom-experience`,
},
{
name: "Cross-Chain Swapping",
href: `${paySlug}/guides/cross-chain-swapping`,
},
],
},
{
+28
View File
@@ -9,6 +9,7 @@ import {
Network,
Rocket,
StickyNote,
Webhook,
Wrench,
} from "lucide-react";
@@ -58,6 +59,33 @@ export const sidebar: SideBar = {
},
],
},
{
name: "Webhooks",
href: `${insightSlug}/webhooks`,
icon: <Webhook />,
links: [
{
name: "Getting Started",
href: `${insightSlug}/webhooks`,
},
{
name: "Managing Webhooks",
href: `${insightSlug}/webhooks/managing-webhooks`,
},
{
name: "Filtering",
href: `${insightSlug}/webhooks/filtering`,
},
{
name: "Payload",
href: `${insightSlug}/webhooks/payload`,
},
{
name: "API Reference",
href: "https://insight-api.thirdweb.com/reference#tag/webhooks",
},
],
},
{
name: "API Reference",
href: "https://insight-api.thirdweb.com/reference",
@@ -0,0 +1,112 @@
import { createMetadata } from "@doc";
export const metadata = createMetadata({
title: "Insight Webhooks | thirdweb Infrastructure",
description: "Filtering",
image: {
title: "Insight",
icon: "insight",
},
});
# Filtering
## Webhook Topics
### `v1.events`
Subscribes to blockchain log events.
### `v1.transactions`
Subscribes to blockchain transactions.
## Webhook Filters
You can filter webhook notifications based on specific criteria.
Each webhook must have either an events filter or a transactions filter (or both).
### Event Filters
```typescript
{
"v1.events": {
chain_ids: string[], // Filter by specific chains
addresses: string[], // Filter by contract addresses
signatures: { // Filter by event signatures
sig_hash: string, // Event signature hash
abi?: string, // Optional ABI for data decoding
params?: Record<string, any> // Filter on decoded parameters
}[]
}
}
```
### Transaction Filters
```typescript
{
"v1.transactions": {
chain_ids: string[], // Filter by specific chains
from_addresses: string[], // Filter by sender addresses
to_addresses: string[], // Filter by recipient addresses
signatures: { // Filter by function signatures
sig_hash: string, // Function signature hash
abi?: string, // Optional ABI for data decoding
params?: string[] // Filter on decoded parameters
}[]
}
}
```
### ABIs
You can specify partial ABIs to have decoded data sent in the webhook payload. For this you also need to give the `sig_hash` of the event or function call.
The following example will filter for `Transfer` events on Ethereum for the contract `0x1f9840a85d5af5bf1d1762f925bdaddc4201f984`
```typescript
{
...
filters: {
"v1.events": {
chain_ids: ["1"],
addresses: ["0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"],
signatures: [
{
sig_hash:
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
abi: '{"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Transfer","type":"event"}',
},
],
},
},
...
}
```
And this example will filter for `Approve` function calls on Ethereum for the contract `0x1f9840a85d5af5bf1d1762f925bdaddc4201f984`
```typescript
{
...
filters: {
"v1.transactions": {
chain_ids: ["1"],
addresses: ["0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"],
signatures: [
{
sig_hash: "0x095ea7b3",
abi: '{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"rawAmount","type":"uint256"}],"name":"approve","type":"function"}',
},
],
},
},
...
}
```
### Params
`params` on the `signatures` object will allow you to filter based on the decoded data. Only strict equality is supported at the moment.
### Notes
- You can specify ABIs to receive decoded event/transaction data
- Parameter filtering supports equality matching only
- At least one filter criteria must be specified
@@ -0,0 +1,68 @@
import { createMetadata } from "@doc";
export const metadata = createMetadata({
title: "Insight Webhooks | thirdweb Infrastructure",
description: "Managing Insight webhooks",
image: {
title: "Insight",
icon: "insight",
},
});
# Managing Webhooks
For most up to date technical spec refer to https://insight-api.thirdweb.com/reference#tag/webhooks.
## List Webhooks
`GET /v1/webhooks`
Retrieve all webhooks for your project or get details for a specific webhook by ID.
## Create Webhook
`POST /v1/webhooks`
Creates a new webhook subscription.
It may take up to a minute to start working.
### Verifying the created webhook
- The webhook starts in a suspended state
- An OTP (One-Time Password) is sent to your webhook URL for verification
- You must verify the webhook within 15 minutes using the OTP
- Once verified, it may take up to a minute for the webhook to become fully active
- You can request a new OTP if you can't retrieve the initial OTP
## Update Webhook
`PATCH /v1/webhooks/:webhook_id`
You can modify the URL or filters of a webhook. Additionally you can enable and disable the webhook.
Changes may take up to a minute to take effect.
### Updating webhook URL
- If you update the webhook URL, you'll need to verify it again with a new OTP
- Other fields can be updated without requiring re-verification
## Delete Webhook
`DELETE /v1/webhooks/:webhook_id`
Permanently removes a webhook subscription. This action cannot be undone.
## Verify Webhook
`POST /v1/webhooks/:webhook_id/verify`
Activates a webhook using the OTP code that was sent to your webhook URL. Required for:
- New webhook creation
- After updating a webhook URL
## Resend OTP
`POST /v1/webhooks/:webhook_id/resend-otp`
Request a new OTP code if the original expires or is lost:
- Invalidates the previous OTP
- New OTP expires after 15 minutes
- Only works for webhooks pending verification
## Test Webhook
`POST /v1/webhooks/test`
Sends a sample payload signed with a test secret ('test123').
Useful for testing your receiver endpoint before creating actual webhooks.
@@ -0,0 +1,33 @@
import { createMetadata } from "@doc";
export const metadata = createMetadata({
title: "Insight Webhooks | thirdweb Infrastructure",
description: "Getting started with Insight webhooks",
image: {
title: "Insight",
icon: "insight",
},
});
# Webhooks
Webhooks allow you to receive notifications when specific blockchain events or transactions occur. This enables you to automate workflows and keep your applications in sync with on-chain activity.
## About Webhooks
### Data Delivery
Webhook events are collected and delivered in batches for optimal performance and reliability. This makes webhooks ideal for:
- Tracking when specific blockchain events occur
- Aggregating on-chain data
- Building analytics and monitoring systems
- Triggering downstream processes
### Delivery Guarantees
Events are guaranteed to be delivered *at least once*.
Your application should implement idempotency and deduplication logic using the unique event ID in the payload.
Events may occasionally be delivered out of order.
### Performance Requirements
- Your receiving endpoint must respond within 3 seconds
- If your endpoint consistently fails to respond in time, the webhook will be automatically suspended
- We recommend implementing a queue system if you need to perform time-consuming operations
@@ -0,0 +1,202 @@
import { createMetadata } from "@doc";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
export const metadata = createMetadata({
title: "Insight Webhooks | thirdweb Infrastructure",
description: "Payload",
image: {
title: "Insight",
icon: "insight",
},
});
# Webhook Payload
## Payload Format
```typescript
{
topic: string, // topic of the data
timestamp: string, // timestamp of when the payload was sent in seconds
data: [
{
data: object, // data of the event or transaction
status: "new" | "reverted",
type: "event" | "transaction",
id: string // unique id of the data
}
]
}
```
Example Response:
<Tabs defaultValue="events">
<TabsList>
<TabsTrigger value="events">Events</TabsTrigger>
<TabsTrigger value="transactions">Transactions</TabsTrigger>
</TabsList>
<TabsContent value='events'>
```json
{
"timestamp": 1743164112,
"topic": "v1.events",
"data": [
{
"data": {
"chain_id": "1",
"block_number": 22140383,
"block_hash": "0x85bd4045d947c34b6b568fc84c7550c0efa741f71c834bbb3d3950e9da27842e",
"block_timestamp": 1743104207,
"transaction_hash": "0x460ced3718d0e09145eae8b63fd985dc366adfc4523c791116f24dc051e8363a",
"transaction_index": 93,
"log_index": 230,
"address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
"data": "0x000000000000000000000000000000000000000000000000009440c61e928cca",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000d360131b77ead72f1f23fb185b4896fe01dc8cb5",
"0x00000000000000000000000085cd07ea01423b1e937929b44e4ad8c40bbb5e71"
],
"decoded": {
"name": "Transfer",
"indexed_params": {
"from": "0xd360131b77ead72f1f23fb185b4896fe01dc8cb5",
"to": "0x85cd07ea01423b1e937929b44e4ad8c40bbb5e71"
},
"non_indexed_params": {
"amount": "41729516213800138"
}
}
},
"status": "new",
"type": "event",
"id": "76b81da4ec46486f0a6e325596f506cad13ebd629520aded104efcaa37a419d5"
}
]
}
```
</TabsContent>
<TabsContent value='transactions'>
```json
{
"timestamp": 1743165030,
"topic": "v1.transactions",
"data": [
{
"data": {
"chain_id": "1",
"hash": "0xceba210be88785cac0884127cb42d370bcde8248e11e490af0deeb6ded2bb421",
"nonce": 5,
"block_hash": "0x86cfe1e32fb3e5091bc2533b78d1503a4539936f50ff5a33b09df639f1572fdd",
"block_number": 22145183,
"block_timestamp": 1743162179,
"transaction_index": 62,
"from_address": "0xcd1785a6a49748948059d3c27d8f55ad0ef94c8c",
"to_address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
"value": "0",
"gas": 50000,
"gas_price": "2000000000",
"data": "0xa9059cbb00000000000000000000000028c6c06298d514db089934071355e5743bf21d60000000000000000000000000000000000000000000000024ac7a2466fe6d4000",
"function_selector": "0xa9059cbb",
"max_fee_per_gas": "2000000000",
"max_priority_fee_per_gas": "2000000000",
"transaction_type": 2,
"r": "93121701149456501424579980283392650303695688563543540003926043202730906996706",
"s": "5712073776861917154895012394334633911928305143620822626852008432451821415527",
"v": "1",
"access_list_json": "[]",
"contract_address": null,
"gas_used": 35318,
"cumulative_gas_used": 9124496,
"effective_gas_price": "2000000000",
"blob_gas_used": 0,
"blob_gas_price": "0",
"logs_bloom": "0x40000000000000000000000000000000000000000020000000000000000000000000000000000000000001000000001000008000000000000000000000000000000000000000000000000008100000000000000000000000000000000000000008000000000000000000000000000000000000000000000002000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"status": 1,
"decoded": {
"name": "transfer",
"inputs": {
"dst": "0x28c6c06298d514db089934071355e5743bf21d60",
"rawAmount": "676511072800000000000"
}
}
},
"status": "new",
"type": "transaction",
"id": "e922999aff6625e1e26e6eca478b2c6ece33a5b8cf3a3c8bde96c00da8d2acc0"
}
]
}
```
</TabsContent>
</Tabs>
## Headers
Each webhook request includes:
- `x-webhook-id` - ID of the webhook configuration to know which one triggered the send
- `x-webhook-signature` - hash of the payload signed with the webhook's secret. Used to validate the payload
## Signature Verification (highly recommended)
To verify that the request is from thirdweb webhooks it is highly recommended to verify the payload.
Each webhook has a `webhook_secret` which is used to sign the raw payload and is then attached to the headers.
To verify the webhook:
```typescript
const generateSignature = (
rawBody: string,
secret: string,
): string => {
return crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
};
const isValidSignature = (
rawBody: string,
signature: string,
secret: string,
): boolean => {
const expectedSignature = generateSignature(
body,
secret,
);
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature),
);
};
// extract the signature from request headers
const signature = req.headers['x-signature'];
// get the raw body of the request
const rawBody = req.rawBody;
const isValid = isValidSignature(rawBody, signature, secret);
```
**You need to use the raw request body when verifying webhooks, as the cryptographic signature is sensitive to even the slightest changes. You should watch out for frameworks that parse the request as JSON and then stringify it because this too will break the signature verification.**
## Payload Age
You can optionally also verify the age of the payload.
After you have verified the signature, you can be sure the timestamp in the payload is correct and apply any time limit you wish.
```typescript
export const isExpired = (
timestamp: string,
expirationInSeconds: number,
): boolean => {
const currentTime = Math.floor(Date.now() / 1000);
return currentTime - parseInt(timestamp) > expirationInSeconds;
};
```
## Reorged Data
If a blockchain reorganization occurs:
- You'll receive an event with `status: "reverted"` instead of `status: "new"`
- You should handle this by reverting any actions taken for the original event
+3 -3
View File
@@ -62,9 +62,9 @@ export default function RootLayout({
<StickyTopContainer>
{/* Note: Please change id as well when changing text or href so that new banner is shown to user even if user dismissed the older one */}
<Banner
id="nebula-alpha"
text="Introducing Nebula - the most powerful AI to interact with the blockchain. Join the waitlist."
href="https://thirdweb.com/nebula"
id="ub-launch"
text="Let users pay with whatever they have without leaving your app"
href="https://thirdweb.com/connect/universal-bridge"
/>
<Header />
</StickyTopContainer>
+22
View File
@@ -20,6 +20,24 @@ Nebula supports reading and writing capabilities on any EVM-compatible chain. [V
Yes, you may ask Nebula questions in any spoken language.
</Details>
<Details summary="How can I make Nebula play a certain character better?">
1. Define the Role Clearly: Specify the character’s role and behavior in your prompt.
Example, “You are an assistant embedded in a blockchain application, designed to provide insights and execute transactions without introducing yourself.”
2. Avoid phrases that suggest personality traits or self-awareness: Use Contextual Instructions and add specific instructions to avoid unnecessary introductions.
Example, “Respond directly to queries without stating your identity or greeting users.”
3. Set Output Expectations: Guide the format of responses.
Example, “Provide concise answers or actions based on the user’s input without additional commentary.”
4. Preload Context: Use Nebula’s context filters to scope responses within a specific domain (e.g., blockchain data or transactions) and avoid generic replies.
5. Session Handling: Utilize Nebula’s session handling feature to maintain relevant context throughout interactions, ensuring responses align with the established character behavior.
</Details>
<Details summary="How can I improve upon Nebula's responses?">
We recommend using [context filters](/nebula/key-concepts/context-filters) to improve relevant responses. You may also modify hyperparameters such as temperature, top-p,
and more using the OpenAI API. Learn more about our [OpenAI API integration](/nebula/plugins/openai).
@@ -46,6 +64,10 @@ The context size or window is 128k tokens or approximately 96,000 words which ma
Nebula is not currently open source. We are exploring open sourcing Nebula in the future.
</Details>
<Details summary="Can Nebula be used in a stateless manner?">
Depending on the use case, Nebula can be used in a stateless manner by not reusing the same session. Nebula learns from each session, so having more contextual requests will be beneficial and are recommended.
</Details>
<Details summary="When will pricing be available for Nebula?">
Pricing options will be available in beta. Nebula is still being tested in alpha.
</Details>
@@ -0,0 +1,93 @@
import { OpenSourceCard, Callout } from "@doc";
# Get Started
Learn how to get started with the thirdweb MCP server.
### Prerequisites
- Python 3.10 or higher
- uv
### Run with uvx
```bash
THIRDWEB_SECRET_KEY=... \
uvx thirdweb-mcp
```
### Install and run with pipx
```bash
pipx install thirdweb-mcp
THIRDWEB_SECRET_KEY=... \
thirdweb-mcp
```
### Install from source
```bash
git clone https://github.com/thirdweb-dev/ai.git thirdweb-ai
cd thirdweb-ai/python/thirdweb-mcp
uv sync
```
## Configuration
The thirdweb MCP server requires configuration based on which services you want to enable:
1. **thirdweb Secret Key**: Required for Nebula and Insight services. Obtain from the [thirdweb dashboard](https://thirdweb.com/dashboard).
2. **Chain IDs**: Blockchain network IDs to connect to (e.g., 1 for Ethereum mainnet, 137 for Polygon).
3. **Engine Configuration**: If using the Engine service, you'll need the Engine URL and authentication JWT.
You can provide these through command-line options or environment variables.
## Usage
### Command-line options
```bash
# Basic usage with default settings (stdio transport with Nebula and Insight)
THIRDWEB_SECRET_KEY=... thirdweb-mcp
# Using SSE transport on a custom port
THIRDWEB_SECRET_KEY=... thirdweb-mcp --transport sse --port 8080
# Enabling all services with specific chain IDs
THIRDWEB_SECRET_KEY=... thirdweb-mcp --chain-id 1 --chain-id 137 \
--engine-url YOUR_ENGINE_URL \
--engine-auth-jwt YOUR_ENGINE_JWT \
--engine-backend-wallet-address YOUR_ENGINE_BACKEND_WALLET_ADDRESS
```
### Environment variables
You can also configure the MCP server using environment variables:
- `THIRDWEB_SECRET_KEY`: Your thirdweb API secret key
- `THIRDWEB_ENGINE_URL`: URL endpoint for thirdweb Engine service
- `THIRDWEB_ENGINE_AUTH_JWT`: Authentication JWT token for Engine
- `THIRDWEB_ENGINE_BACKEND_WALLET_ADDRESS`: Wallet address for Engine backend
## Available Services
### Nebula
Autonomous onchain execution and analysis:
- Analyze smart contract code
- Contract interactions and deployments
- Autonomous onchain tasks execution
### Insight
Offers blockchain data analysis capabilities:
- Query on-chain data across multiple networks
- Analyze transactions, blocks, and smart contract events
- Monitor wallet activities and token movements
### Engine
Integrates with thirdweb's backend infrastructure:
- Deploy smart contracts
- Interact with deployed contracts
- Manage wallet connections and transactions
@@ -0,0 +1,31 @@
# Claude Desktop
Learn how to add the MCP server to Claude Desktop:
1. Install the MCP: `pipx install thirdweb-mcp`
2. Create or edit the Claude Desktop configuration file at:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
- Linux: `~/.config/Claude/claude_desktop_config.json`
3. Add the following configuration:
```json
{
"mcpServers": {
"thirdweb-mcp": {
"command": "thirdweb-mcp",
"args": [], // add `--chain-id` optionally
"env": {
"THIRDWEB_SECRET_KEY": "your thirdweb secret key from dashboard",
"THIRDWEB_ENGINE_URL": "(OPTIONAL) your engine url",
"THIRDWEB_ENGINE_AUTH_JWT": "(OPTIONAL) your engine auth jwt",
"THIRDWEB_ENGINE_BACKEND_WALLET_ADDRESS": "(OPTIONAL) your engine backend wallet address",
},
}
}
}
```
4. Restart Claude Desktop for the changes to take effect.
@@ -0,0 +1,10 @@
### MCP Clients
Learn how to integrate the thirdweb MCP server with various clients.
This server can be integrated with any client that supports the Model Context Protocol:
1. Run the MCP server with the appropriate configuration
2. Connect your MCP client to the server using the selected transport (stdio or SSE)
3. Access thirdweb services through the exposed MCP tools

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