Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
c547164320 Version Packages (#6417)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-05 17:37:39 -08:00
Joaquim VergesandGitHub 38ae2d3c6b [SDK] expose getUserOpHash utility function (#6424) 2025-03-05 17:08:15 -08:00
jnsdls d75320191d update ecosystem API routes to include team slug (#6422)
Updates ecosystem API endpoints to be scoped under team routes

The ecosystem wallet API endpoints have been updated to be scoped under team routes, ensuring proper data isolation between teams. All ecosystem-related API calls now include the team ID or slug in the path.

Key changes:
- Updated API endpoints from `/v1/ecosystem-wallet/*` to `/v1/teams/{teamIdOrSlug}/ecosystem-wallet/*`
- Added team slug parameter to ecosystem fetch and list operations
- Modified React hooks to include team context in query keys
- Updated component props to pass team information through the component tree

This change ensures ecosystem wallets are properly scoped to their respective teams and maintains data isolation between different team contexts.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates various functions and components within the ecosystem module to include `team_slug` as a parameter. This change enhances the ability to fetch ecosystems and their details specific to a team, improving data handling across the application.

### Detailed summary
- Updated `fetchEcosystem` to accept `team_slug`.
- Modified `fetchEcosystemList` to include `team_slug`.
- Adjusted the `useEcosystemList` hook to utilize `teamIdOrSlug`.
- Enhanced `useEcosystem` to accept `teamIdOrSlug`.
- Updated components to pass `team_slug` where necessary.
- Changed redirect logic to incorporate team-specific ecosystems.

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

<!-- end pr-codex -->
2025-03-05 19:00:54 +00:00
saminacodes 6046d4510d [Portal] Universal Bridge Docs (#6421)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on renaming and updating the documentation related to `thirdweb Pay`, transitioning to the `Universal Bridge` terminology. It enhances clarity and alignment with the new features and guides for web3 payments.

### Detailed summary
- Updated titles and descriptions in various `.mdx` files to reflect `Universal Bridge`.
- Changed sidebar navigation names and links to align with `Universal Bridge`.
- Revised content in `page.mdx` files to include new functionalities and guides.
- Added sections on fee sharing and supported chains in the `Overview` documentation.
- Modified FAQ entries to clarify `Universal Bridge` specifics.

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

<!-- end pr-codex -->
2025-03-05 18:28:34 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f1d32261f0 chore(deps-dev): bump eslint-plugin-react-compiler from 19.0.0-beta-e1e972c-20250221 to 19.0.0-beta-40c6c23-20250301 (#6395)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 09:19:51 -08:00
Manan TankandGitHub 04baa74ad7 [TOOL-3610] Dashboard: Fix z-index issue with network selector on chakra modal (#6418) 2025-03-05 07:51:33 -08:00
Phillip HoandGitHub 44bb0034a6 [service-utils] Add pay as a usage source (#6416) 2025-03-05 18:51:39 +08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
cfeba37e40 Version Packages (#6410)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-03-05 02:15:40 -08:00
Joaquim VergesandGitHub fb446bd099 [SDK] feat: Add transfer success callback (#6415) 2025-03-05 02:09:02 -08:00
jnsdls 24063af948 update engine API routes to include team slug (#6407)
blocked by: https://app.graphite.dev/github/pr/thirdweb-dev/api-server/1437/add-team-engine-management-endpoints

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the handling of `teamIdOrSlug` across various components and utility functions related to engine instances and alerts, ensuring that the correct team context is maintained throughout the application.

### Detailed summary
- Added `teamIdOrSlug` parameter to multiple functions and components.
- Updated API endpoints to include `teamIdOrSlug` in fetch calls.
- Modified components like `EngineAlertsPage`, `EngineSystem`, and `EngineInstancesTable` to pass `teamIdOrSlug`.
- Adjusted data structure handling in API responses to use `result` instead of `data`.

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

<!-- end pr-codex -->
2025-03-05 01:39:38 +00:00
MananTank e2c9d4de34 [TOOL-3606] playground: Remove Ethereum as default selected chain in insight playground (#6413)
<!-- start pr-codex -->

## PR-Codex overview
This PR refines the logic for setting default values in the `values` object within the `blueprint-playground.client.tsx` file. It consolidates the conditions for assigning a default value to parameters.

### Detailed summary
- Removed the specific check for `param.name === "chain"` that assigned a hardcoded value of `"1"`.
- Simplified the condition to check if `param.schema` has a `type` and a `default` property.
- Updated the assignment of `values[param.name]` to use `param.schema.default` when applicable, otherwise setting it to an empty string.

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

<!-- end pr-codex -->
2025-03-05 01:14:05 +00:00
jnsdls 0f6aa6b85d add /faucets to framer rewrites (#6414)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on adding a new route to the `framer-rewrites.js` file for redirecting to the `/faucets` page.

### Detailed summary
- Added the route `"/faucets"` to the array of paths in `apps/dashboard/framer-rewrites.js`.

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

<!-- end pr-codex -->
2025-03-05 01:06:38 +00:00
Joaquim VergesandGitHub 9f1a77f4c4 [SDK] fix: Allow resending OTP on failures (#6412) 2025-03-04 16:58:30 -08:00
Manan TankandGitHub 158a08aafc [TOOL-3598] Dashboard: Add error message in create ecosystem form for image size (#6411) 2025-03-04 12:16:25 -08:00
Phillip HoandGitHub 67354dc848 [service-utils] remove unneeded cf worker imports (#6409) 2025-03-04 21:57:02 +08:00
Joaquim VergesandGitHub 575b7b3a58 [SDK] feat: update zksync chain checks (#6408) 2025-03-04 01:50:43 -08:00
Jonas DanielsandGitHub 35ccf8f2e8 updated dependencies (#6404) 2025-03-03 20:19:54 -08:00
MananTank b5e02a4f10 [TOOl-3590] Dashboard: Remove unused components and stories (#6406)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on removing unused components and simplifying the `CouponCard` functionality by streamlining the handling of coupon applications and eliminating the modal for sharing free wallets.

### Detailed summary
- Deleted multiple files related to `share-free-wallets-modal` and analytics components.
- Removed the dynamic import of `LazyShareFreeWalletsModal` in `CouponCard.tsx`.
- Simplified the `onCouponApplied` function signature by removing the `isFreeWalletsCoupon` parameter.
- Eliminated state management for `showShareModal` in `CouponSection`.

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

<!-- end pr-codex -->
2025-03-04 01:47:57 +00:00
kumaryash90 cb71dd597e Update implementations (#6405)
<!--

## 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 updates the `MarketplaceV3` addresses in the `ZKSYNC_IMPLEMENTATIONS` object for various entries to new contract addresses.

### Detailed summary
- Updated `MarketplaceV3` address from `0xBc02441a36Bb4029Cd191b20243c2e41B862F118` to `0x88e7F2bEf0a23073b7Cc2c79D307Dbd9138Fa79c` at index `324`.
- Updated `MarketplaceV3` address from `0x4027561E163a420c4e5Db46E07EBd581CAa8Bb62` to `0x4b14569c7B79DBe686Ac3Ba5996131E7EDaB7a93` at index `2741`.

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

<!-- end pr-codex -->
2025-03-04 00:03:26 +00:00
Alec AnanianandGitHub 83ec6957f9 playground: fix pay example imports (#6391) 2025-03-03 15:56:36 -08:00
kumaryash90 c0bbc21a84 Update deploy configs (#6299)
TOOL-3478

<!--

## 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 platform fee configuration in the contract deployment process, introducing a new fee constant, and modifying the logic to handle different fee scenarios based on the publisher address.

### Detailed summary
- Added `THIRDWEB_PUBLISHER_ADDRESS` constant in `addresses.ts`.
- Introduced `DEFAULT_FEE_BPS_NEW` constant in `addresses.ts`.
- Updated `PlatformFeeFieldset` to destructure `isMarketplace`.
- Modified `CustomContractForm` to conditionally use `DEFAULT_FEE_BPS_NEW` based on the publisher address.
- Adjusted imports in `custom-contract.tsx` to include new constants.

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

<!-- end pr-codex -->
2025-03-03 22:09:24 +00:00
Jonas DanielsandGitHub 9fcf1b80de skip smart wallet tests without TW_SECRET_KEY (#6403) 2025-03-03 13:43:07 -08:00
Jonas DanielsandGitHub 33ddf56a0a remove secretHash from rotate key response (#6401) 2025-03-03 13:32:38 -08:00
MananTank f9fa5df6b1 [TOOl-3587] Dashboard: Add in-app-wallet login mode (#6400)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces enhancements to the `LoginAndOnboardingPage` by adding support for an in-app wallet login option and refactoring the login handling logic.

### Detailed summary
- Added `loginWithInAppWallet` prop to `LoginAndOnboardingPage` and `LoginAndOnboardingPageContent`.
- Updated `Page` component to handle `in-app-wallet` search parameter.
- Introduced `inAppWalletLoginOptions` for in-app wallet authentication.
- Modified wallet selection logic based on `loginWithInAppWallet` prop.

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

<!-- end pr-codex -->
2025-03-03 21:17:31 +00:00
MananTank 0d82fc0dd1 [TOOL-3584] Dashboad: Move turnstile widget above connect embed in login page (#6394) 2025-03-03 20:21:59 +00:00
MananTank ef6029b939 [TOOL-3575] Dashboard: Fix /home link 404 after upgrading to nextjs 15.2 (#6393)
<!--

## 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 updates routing and links in the dashboard application to improve navigation.

### Detailed summary
- In `apps/dashboard/next.config.ts`, the `destination` was changed from `"/"` to `"https://landing.thirdweb.com"`.
- In `apps/dashboard/src/app/components/MobileBurgerMenuButton.tsx`, the `href` for the `Link` component was updated from `"/"` to `"/home"`.

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

<!-- end pr-codex -->
2025-03-03 19:56:41 +00:00
MananTank 004601812a [TOOL-3581] Dashboard: Fix project overview page crashing because of ContractDistributionCard (#6392) 2025-03-03 18:39:45 +00:00
97 changed files with 2941 additions and 4271 deletions
+2
View File
@@ -55,4 +55,6 @@ module.exports = [
"/learn/guides/:guide_slug",
"/learn/courses",
"/learn/glossary",
// -- faucets --
"/faucets",
];
+1 -1
View File
@@ -153,7 +153,7 @@ const baseNextConfig: NextConfig = {
// re-write /home to / (this is so that logged in users will be able to go to /home and NOT be redirected to the logged in app)
{
source: "/home",
destination: "/",
destination: "https://landing.thirdweb.com",
},
...FRAMER_PATHS.map((path) => ({
source: path,
+21 -21
View File
@@ -45,9 +45,9 @@
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tooltip": "1.1.8",
"@sentry/nextjs": "9.2.0",
"@shazow/whatsabi": "^0.19.0",
"@tanstack/react-query": "5.66.9",
"@sentry/nextjs": "9.3.0",
"@shazow/whatsabi": "0.20.0",
"@tanstack/react-query": "5.67.1",
"@tanstack/react-table": "^8.21.2",
"@thirdweb-dev/service-utils": "workspace:*",
"@vercel/functions": "2.0.0",
@@ -61,13 +61,13 @@
"date-fns": "4.1.0",
"fetch-event-stream": "0.1.5",
"flat": "^6.0.1",
"framer-motion": "12.4.7",
"framer-motion": "12.4.10",
"fuse.js": "7.1.0",
"idb-keyval": "^6.2.1",
"input-otp": "^1.4.1",
"ioredis": "^5.5.0",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.476.0",
"lucide-react": "0.477.0",
"next": "15.2.0",
"next-plausible": "^3.12.4",
"next-themes": "^0.4.4",
@@ -94,7 +94,7 @@
"shiki": "1.27.0",
"sonner": "2.0.1",
"spdx-correct": "^3.2.0",
"swagger-ui-react": "^5.19.0",
"swagger-ui-react": "^5.20.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
@@ -104,21 +104,21 @@
},
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "3.2.4",
"@chromatic-com/storybook": "3.2.5",
"@next/bundle-analyzer": "15.2.0",
"@next/eslint-plugin-next": "15.2.0",
"@playwright/test": "1.50.1",
"@storybook/addon-essentials": "8.6.0",
"@storybook/addon-interactions": "8.6.0",
"@storybook/addon-links": "8.6.0",
"@storybook/addon-onboarding": "8.6.0",
"@storybook/addon-viewport": "8.6.0",
"@storybook/blocks": "8.6.0",
"@storybook/nextjs": "8.6.0",
"@storybook/react": "8.6.0",
"@storybook/test": "8.6.0",
"@storybook/addon-essentials": "8.6.3",
"@storybook/addon-interactions": "8.6.3",
"@storybook/addon-links": "8.6.3",
"@storybook/addon-onboarding": "8.6.3",
"@storybook/addon-viewport": "8.6.3",
"@storybook/blocks": "8.6.3",
"@storybook/nextjs": "8.6.3",
"@storybook/react": "8.6.3",
"@storybook/test": "8.6.3",
"@types/color": "4.2.0",
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"@types/papaparse": "^5.3.15",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
@@ -130,16 +130,16 @@
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.19",
"checkly": "^4.19.1",
"checkly": "5.0.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
"eslint-plugin-storybook": "0.11.3",
"knip": "5.45.0",
"next-sitemap": "^4.2.3",
"postcss": "8.5.3",
"storybook": "8.6.0",
"storybook": "8.6.3",
"tailwindcss": "3.4.17",
"typescript": "5.7.3"
"typescript": "5.8.2"
}
}
+7
View File
@@ -172,3 +172,10 @@ input:-webkit-autofill:active {
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
/* Remove this when chakra is removed */
/* Make chakra modals have same z-index as shadcn dialog */
body {
--chakra-zIndices-modal: 50;
--chakra-zIndices-overlay: 50;
}
@@ -371,7 +371,6 @@ export type RotateSecretKeyAPIReturnType = {
data: {
secret: string;
secretMasked: string;
secretHash: string;
};
};
@@ -232,13 +232,15 @@ export function useEngineUpdateDeployment() {
export type RemoveEngineFromDashboardIParams = {
instanceId: string;
teamIdOrSlug: string;
};
export async function removeEngineFromDashboard({
instanceId,
teamIdOrSlug,
}: RemoveEngineFromDashboardIParams) {
const res = await apiServerProxy({
pathname: `/v1/engine/${instanceId}`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${instanceId}`,
method: "DELETE",
});
@@ -273,18 +275,20 @@ export async function deleteCloudHostedEngine({
}
export type EditEngineInstanceParams = {
teamIdOrSlug: string;
instanceId: string;
name: string;
url: string;
};
export async function editEngineInstance({
teamIdOrSlug,
instanceId,
name,
url,
}: EditEngineInstanceParams) {
const res = await apiServerProxy({
pathname: `/v1/engine/${instanceId}`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${instanceId}`,
method: "PUT",
headers: {
"Content-Type": "application/json",
@@ -1628,7 +1632,7 @@ export function useEngineSubscriptionsLastBlock(params: {
interface EngineResourceMetrics {
error: string;
data: {
result: {
cpu: number;
memory: number;
errorRate: ResultItem[];
@@ -1637,7 +1641,7 @@ interface EngineResourceMetrics {
};
}
export function useEngineSystemMetrics(engineId: string) {
export function useEngineSystemMetrics(engineId: string, teamIdOrSlug: string) {
const [enabled, setEnabled] = useState(true);
return useQuery({
@@ -1645,7 +1649,7 @@ export function useEngineSystemMetrics(engineId: string) {
queryFn: async () => {
const res = await apiServerProxy({
method: "GET",
pathname: `/v1/engine/${engineId}/metrics`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/metrics`,
});
if (!res.ok) {
@@ -1674,14 +1678,14 @@ export interface EngineAlertRule {
pausedAt: Date | null;
}
export function useEngineAlertRules(engineId: string) {
export function useEngineAlertRules(engineId: string, teamIdOrSlug: string) {
return useQuery({
queryKey: engineKeys.alertRules(engineId),
queryFn: async () => {
const res = await apiServerProxy<{
data: EngineAlertRule[];
}>({
pathname: `/v1/engine/${engineId}/alert-rules`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/alert-rules`,
method: "GET",
});
@@ -1703,14 +1707,19 @@ export interface EngineAlert {
endsAt: Date | null;
}
export function useEngineAlerts(engineId: string, limit: number, offset = 0) {
export function useEngineAlerts(
engineId: string,
teamIdOrSlug: string,
limit: number,
offset = 0,
) {
return useQuery({
queryKey: engineKeys.alerts(engineId),
queryFn: async () => {
const res = await apiServerProxy<{
data: EngineAlert[];
}>({
pathname: `/v1/engine/${engineId}/alerts`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/alerts`,
searchParams: {
limit: `${limit}`,
offset: `${offset}`,
@@ -1754,14 +1763,17 @@ export type EngineNotificationChannel = {
subscriptionRoutes: string[];
};
export function useEngineNotificationChannels(engineId: string) {
export function useEngineNotificationChannels(
engineId: string,
teamIdOrSlug: string,
) {
return useQuery({
queryKey: engineKeys.notificationChannels(engineId),
queryFn: async () => {
const res = await apiServerProxy<{
data: EngineNotificationChannel[];
}>({
pathname: `/v1/engine/${engineId}/notification-channels`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/notification-channels`,
method: "GET",
});
@@ -1781,7 +1793,10 @@ export interface CreateNotificationChannelInput {
value: string;
}
export function useEngineCreateNotificationChannel(engineId: string) {
export function useEngineCreateNotificationChannel(
engineId: string,
teamIdOrSlug: string,
) {
const queryClient = useQueryClient();
return useMutation({
@@ -1789,7 +1804,7 @@ export function useEngineCreateNotificationChannel(engineId: string) {
const res = await apiServerProxy<{
data: EngineNotificationChannel;
}>({
pathname: `/v1/engine/${engineId}/notification-channels`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/notification-channels`,
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -1812,13 +1827,16 @@ export function useEngineCreateNotificationChannel(engineId: string) {
});
}
export function useEngineDeleteNotificationChannel(engineId: string) {
export function useEngineDeleteNotificationChannel(
engineId: string,
teamIdOrSlug: string,
) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (notificationChannelId: string) => {
const res = await apiServerProxy({
pathname: `/v1/engine/${engineId}/notification-channels/${notificationChannelId}`,
pathname: `/v1/teams/${teamIdOrSlug}/engine/${engineId}/notification-channels/${notificationChannelId}`,
method: "DELETE",
});
@@ -166,7 +166,7 @@ export function MobileBurgerMenuButton(
</Link>
<Link
href="/"
href="/home"
className="text-base text-muted-foreground hover:text-foreground"
>
Home Page
+43 -13
View File
@@ -24,7 +24,7 @@ const LazyOnboardingUI = lazy(
() => import("./onboarding/on-boarding-ui.client"),
);
const wallets = [
const loginOptions = [
inAppWallet({
auth: {
options: [
@@ -45,9 +45,27 @@ const wallets = [
createWallet("io.zerion.wallet"),
];
const inAppWalletLoginOptions = [
inAppWallet({
auth: {
options: [
"google",
"apple",
"facebook",
"github",
"email",
"phone",
"passkey",
"wallet",
],
},
}),
];
export function LoginAndOnboardingPage(props: {
account: Account | undefined;
redirectPath: string;
loginWithInAppWallet: boolean;
}) {
return (
<div className="relative flex min-h-dvh flex-col overflow-hidden bg-background">
@@ -91,6 +109,7 @@ export function LoginAndOnboardingPage(props: {
<LoginAndOnboardingPageContent
account={props.account}
redirectPath={props.redirectPath}
loginWithInAppWallet={props.loginWithInAppWallet}
/>
</div>
);
@@ -99,6 +118,7 @@ export function LoginAndOnboardingPage(props: {
export function LoginAndOnboardingPageContent(props: {
account: Account | undefined;
redirectPath: string;
loginWithInAppWallet: boolean;
}) {
return (
<div className="relative flex grow flex-col">
@@ -114,6 +134,7 @@ export function LoginAndOnboardingPageContent(props: {
<PageContent
redirectPath={props.redirectPath}
account={props.account}
loginWithInAppWallet={props.loginWithInAppWallet}
/>
</ClientOnly>
</main>
@@ -139,6 +160,7 @@ function LoadingCard() {
function PageContent(props: {
redirectPath: string;
account: Account | undefined;
loginWithInAppWallet: boolean;
}) {
const [screen, setScreen] = useState<
| { id: "login" }
@@ -190,7 +212,12 @@ function PageContent(props: {
}
if (connectionStatus !== "connected" || screen.id === "login") {
return <CustomConnectEmbed onLogin={onLogin} />;
return (
<CustomConnectEmbed
onLogin={onLogin}
loginWithInAppWallet={props.loginWithInAppWallet}
/>
);
}
if (screen.id === "onboarding") {
@@ -215,6 +242,7 @@ function PageContent(props: {
function CustomConnectEmbed(props: {
onLogin: () => void;
loginWithInAppWallet: boolean;
}) {
const { theme } = useTheme();
const client = useThirdwebClient();
@@ -222,6 +250,16 @@ function CustomConnectEmbed(props: {
return (
<div className="flex flex-col items-center gap-4">
<Turnstile
options={{
// only show if interaction is required
appearance: "interaction-only",
// match the theme of the rest of the app
theme: theme === "light" ? "light" : "dark",
}}
siteKey={TURNSTILE_SITE_KEY}
onSuccess={(token) => setTurnstileToken(token)}
/>
<ConnectEmbed
auth={{
getLoginPayload,
@@ -247,7 +285,9 @@ function CustomConnectEmbed(props: {
return isLoggedInResult;
},
}}
wallets={wallets}
wallets={
props.loginWithInAppWallet ? inAppWalletLoginOptions : loginOptions
}
client={client}
modalSize="wide"
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
@@ -255,16 +295,6 @@ function CustomConnectEmbed(props: {
privacyPolicyUrl="/privacy-policy"
termsOfServiceUrl="/terms"
/>
<Turnstile
options={{
// only show if interaction is required
appearance: "interaction-only",
// match the theme of the rest of the app
theme: theme === "light" ? "light" : "dark",
}}
siteKey={TURNSTILE_SITE_KEY}
onSuccess={(token) => setTurnstileToken(token)}
/>
</div>
);
}
+10 -3
View File
@@ -4,10 +4,13 @@ import { isValidEncodedRedirectPath } from "./isValidEncodedRedirectPath";
export default async function Page(props: {
searchParams: Promise<{
next?: string;
next: string | string[] | undefined;
"in-app-wallet": string | string[] | undefined;
}>;
}) {
const nextPath = (await props.searchParams).next;
const searchParams = await props.searchParams;
const nextPath =
typeof searchParams.next === "string" ? searchParams.next : undefined;
const account = await getRawAccount();
// don't redirect away from login page if authToken is already present and onboarding is done
@@ -20,6 +23,10 @@ export default async function Page(props: {
nextPath && isValidEncodedRedirectPath(nextPath) ? nextPath : "/team";
return (
<LoginAndOnboardingPage account={account} redirectPath={redirectPath} />
<LoginAndOnboardingPage
account={account}
redirectPath={redirectPath}
loginWithInAppWallet={searchParams["in-app-wallet"] === "true"}
/>
);
}
@@ -53,6 +53,7 @@ export function NebulaLoginPage(props: {
{showPage === "connect" && (
<LoginAndOnboardingPageContent
loginWithInAppWallet={false}
account={props.account}
redirectPath={
message ? `/?prompt=${encodeURIComponent(message)}` : "/"
@@ -28,7 +28,7 @@ export default async function Page(props: {
}
const [ecosystem, team] = await Promise.all([
fetchEcosystem(params.slug, authToken),
fetchEcosystem(params.slug, authToken, params.team_slug),
getTeamBySlug(params.team_slug),
]);
@@ -10,7 +10,7 @@ export async function EcosystemLayoutSlug({
ecosystemLayoutPath,
}: {
children: React.ReactNode;
params: { slug: string };
params: { slug: string; team_slug: string };
ecosystemLayoutPath: string;
}) {
const authToken = await getAuthToken();
@@ -19,7 +19,11 @@ export async function EcosystemLayoutSlug({
redirect(ecosystemLayoutPath);
}
const ecosystem = await fetchEcosystem(params.slug, authToken);
const ecosystem = await fetchEcosystem(
params.slug,
authToken,
params.team_slug,
);
if (!ecosystem) {
redirect(ecosystemLayoutPath);
@@ -30,6 +34,7 @@ export async function EcosystemLayoutSlug({
<EcosystemHeader
ecosystem={ecosystem}
ecosystemLayoutPath={ecosystemLayoutPath}
teamIdOrSlug={params.team_slug}
/>
<SidebarLayout
@@ -62,8 +62,11 @@ function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) {
function EcosystemSelect(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
teamIdOrSlug: string;
}) {
const { data: ecosystems, isPending } = useEcosystemList();
const { data: ecosystems, isPending } = useEcosystemList({
teamIdOrSlug: props.teamIdOrSlug,
});
return isPending ? (
<Skeleton className="h-10 w-full md:w-[160px]" />
@@ -109,8 +112,10 @@ function EcosystemSelect(props: {
export function EcosystemHeader(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
teamIdOrSlug: string;
}) {
const { data: fetchedEcosystem } = useEcosystem({
teamIdOrSlug: props.teamIdOrSlug,
slug: props.ecosystem.slug,
refetchInterval:
props.ecosystem.status === "requested"
@@ -192,6 +197,7 @@ export function EcosystemHeader(props: {
<EcosystemSelect
ecosystem={ecosystem}
ecosystemLayoutPath={props.ecosystemLayoutPath}
teamIdOrSlug={props.teamIdOrSlug}
/>
</div>
</div>
@@ -7,8 +7,11 @@ import { IntegrationPermissionsSection } from "../server/integration-permissions
export function EcosystemPermissionsPage({
params,
authToken,
}: { params: { slug: string }; authToken: string }) {
const { data: ecosystem } = useEcosystem({ slug: params.slug });
}: { params: { slug: string; team_slug: string }; authToken: string }) {
const { data: ecosystem } = useEcosystem({
slug: params.slug,
teamIdOrSlug: params.team_slug,
});
return (
<div className="flex flex-col gap-8">
@@ -3,21 +3,23 @@ import { useQuery } from "@tanstack/react-query";
import type { Ecosystem } from "../../../types";
export function useEcosystem({
teamIdOrSlug,
slug,
refetchInterval,
refetchOnWindowFocus,
initialData,
}: {
teamIdOrSlug: string;
slug: string;
refetchInterval?: number;
refetchOnWindowFocus?: boolean;
initialData?: Ecosystem;
}) {
return useQuery({
queryKey: ["ecosystems", slug],
queryKey: ["ecosystems", teamIdOrSlug, slug],
queryFn: async () => {
const res = await apiServerProxy({
pathname: `/v1/ecosystem-wallet/${slug}`,
pathname: `/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
method: "GET",
});
@@ -31,9 +31,13 @@ const formSchema = z.object({
.refine((name) => /^[a-zA-Z0-9 ]*$/.test(name), {
message: "Name can only contain letters, numbers and spaces",
}),
logo: z.instanceof(File, {
message: "Logo is required",
}),
logo: z
.instanceof(File, {
message: "Logo is required",
})
.refine((file) => file.size <= 500 * 1024, {
message: "Logo size must be less than 500KB",
}),
permission: z.union([z.literal("PARTNER_WHITELIST"), z.literal("ANYONE")]),
});
@@ -118,8 +122,9 @@ export function CreateEcosystemForm(props: { teamSlug: string }) {
accept="image/png, image/jpeg"
onUpload={(files) => {
if (files[0]) {
form.setValue("logo", files[0]);
form.clearErrors("logo");
form.setValue("logo", files[0], {
shouldValidate: true,
});
}
}}
/>
@@ -3,13 +3,17 @@ import { useQuery } from "@tanstack/react-query";
import { useActiveAccount } from "thirdweb/react";
import type { Ecosystem } from "../types";
export function useEcosystemList() {
export function useEcosystemList({
teamIdOrSlug,
}: {
teamIdOrSlug: string;
}) {
const address = useActiveAccount()?.address;
return useQuery({
queryKey: ["ecosystems", address],
queryKey: ["ecosystems", teamIdOrSlug, address],
queryFn: async () => {
const res = await apiServerProxy({
pathname: "/v1/ecosystem-wallet/list",
pathname: `/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
method: "GET",
});
@@ -20,10 +20,12 @@ export default async function Page(props: {
loginRedirect(ecosystemLayoutPath);
}
const ecosystems = await fetchEcosystemList(authToken).catch((err) => {
console.error("failed to fetch ecosystems", err);
return [];
});
const ecosystems = await fetchEcosystemList(authToken, team_slug).catch(
(err) => {
console.error("failed to fetch ecosystems", err);
return [];
},
);
if (ecosystems[0]) {
redirect(`${ecosystemLayoutPath}/${ecosystems[0].slug}`);
}
@@ -1,12 +1,19 @@
import { API_SERVER_URL } from "@/constants/env";
import type { Ecosystem } from "../types";
export async function fetchEcosystem(slug: string, authToken: string) {
const res = await fetch(`${API_SERVER_URL}/v1/ecosystem-wallet/${slug}`, {
headers: {
Authorization: `Bearer ${authToken}`,
export async function fetchEcosystem(
slug: string,
authToken: string,
teamIdOrSlug: string,
) {
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${teamIdOrSlug}/ecosystem-wallet/${slug}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
});
);
if (!res.ok) {
const data = await res.json();
console.error(data);
@@ -1,12 +1,18 @@
import { API_SERVER_URL } from "@/constants/env";
import type { Ecosystem } from "../types";
export async function fetchEcosystemList(authToken: string) {
const res = await fetch(`${API_SERVER_URL}/v1/ecosystem-wallet/list`, {
headers: {
Authorization: `Bearer ${authToken}`,
export async function fetchEcosystemList(
authToken: string,
teamIdOrSlug: string,
) {
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${teamIdOrSlug}/ecosystem-wallet`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
});
);
if (!res.ok) {
const data = await res.json();
@@ -28,12 +28,15 @@ const formSchema = z.object({
type ImportEngineParams = z.infer<typeof formSchema>;
async function importEngine(data: ImportEngineParams) {
async function importEngine({
teamIdOrSlug,
...data
}: ImportEngineParams & { teamIdOrSlug: string }) {
// Instance URLs should end with a /.
const url = data.url.endsWith("/") ? data.url : `${data.url}/`;
const res = await apiServerProxy({
pathname: "/v1/engine",
pathname: `/v1/teams/${teamIdOrSlug}/engine`,
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -59,7 +62,7 @@ export function EngineImportCard(props: {
<EngineImportCardUI
prefillImportUrl={props.prefillImportUrl}
importEngine={async (params) => {
await importEngine(params);
await importEngine({ ...params, teamIdOrSlug: props.teamSlug });
router.push(`/team/${props.teamSlug}/~/engine`);
}}
/>
@@ -74,6 +74,7 @@ type RemovedEngineFromDashboard = (
) => Promise<void>;
export function EngineInstancesTable(props: {
teamIdOrSlug: string;
instances: EngineInstance[];
engineLinkPrefix: string;
}) {
@@ -81,6 +82,7 @@ export function EngineInstancesTable(props: {
return (
<EngineInstancesTableUI
teamIdOrSlug={props.teamIdOrSlug}
instances={props.instances}
engineLinkPrefix={props.engineLinkPrefix}
deleteCloudHostedEngine={async (params) => {
@@ -100,6 +102,7 @@ export function EngineInstancesTable(props: {
}
export function EngineInstancesTableUI(props: {
teamIdOrSlug: string;
instances: EngineInstance[];
engineLinkPrefix: string;
deleteCloudHostedEngine: DeletedCloudHostedEngine;
@@ -124,6 +127,7 @@ export function EngineInstancesTableUI(props: {
{props.instances.map((instance) => (
<EngineInstanceRow
key={instance.id}
teamIdOrSlug={props.teamIdOrSlug}
instance={instance}
engineLinkPrefix={props.engineLinkPrefix}
deleteCloudHostedEngine={props.deleteCloudHostedEngine}
@@ -145,6 +149,7 @@ export function EngineInstancesTableUI(props: {
}
function EngineInstanceRow(props: {
teamIdOrSlug: string;
instance: EngineInstance;
engineLinkPrefix: string;
deleteCloudHostedEngine: DeletedCloudHostedEngine;
@@ -187,6 +192,7 @@ function EngineInstanceRow(props: {
</TableRow>
<EditModal
teamIdOrSlug={props.teamIdOrSlug}
instance={instance}
open={isEditModalOpen}
onOpenChange={setIsEditModalOpen}
@@ -194,6 +200,7 @@ function EngineInstanceRow(props: {
/>
<RemoveModal
teamIdOrSlug={props.teamIdOrSlug}
instance={instance}
onOpenChange={setIsRemoveModalOpen}
open={isRemoveModalOpen}
@@ -349,6 +356,7 @@ function EngineActionsDropdown(props: {
function EditModal(props: {
open: boolean;
teamIdOrSlug: string;
onOpenChange: (open: boolean) => void;
instance: EngineInstance;
editEngineInstance: EditedEngineInstance;
@@ -357,6 +365,7 @@ function EditModal(props: {
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
<DialogContent className="overflow-hidden p-0">
<EditModalContent
teamIdOrSlug={props.teamIdOrSlug}
instance={props.instance}
editEngineInstance={props.editEngineInstance}
closeModal={() => props.onOpenChange(false)}
@@ -372,6 +381,7 @@ const editEngineFormSchema = z.object({
});
function EditModalContent(props: {
teamIdOrSlug: string;
instance: EngineInstance;
editEngineInstance: EditedEngineInstance;
closeModal: () => void;
@@ -396,6 +406,7 @@ function EditModalContent(props: {
onSubmit={form.handleSubmit((data) =>
editInstance.mutate(
{
teamIdOrSlug: props.teamIdOrSlug,
instanceId: props.instance.id,
name: data.name,
url: data.url,
@@ -475,6 +486,7 @@ function EditModalContent(props: {
}
function RemoveModal(props: {
teamIdOrSlug: string;
instance: EngineInstance;
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -491,6 +503,7 @@ function RemoveModal(props: {
(instance.status === "active" && !instance.deploymentId) ? (
<RemoveEngineFromDashboardModalContent
instance={instance}
teamIdOrSlug={props.teamIdOrSlug}
close={() => onOpenChange(false)}
removeEngineFromDashboard={props.removeEngineFromDashboard}
/>
@@ -507,6 +520,7 @@ function RemoveModal(props: {
}
function RemoveEngineFromDashboardModalContent(props: {
teamIdOrSlug: string;
instance: EngineInstance;
close: () => void;
removeEngineFromDashboard: RemovedEngineFromDashboard;
@@ -552,6 +566,7 @@ function RemoveEngineFromDashboardModalContent(props: {
removeFromDashboard.mutate(
{
instanceId: instance.id,
teamIdOrSlug: props.teamIdOrSlug,
},
{
onSuccess: () => {
@@ -12,6 +12,7 @@ export const EngineInstancesList = (props: {
return (
<div className="flex grow flex-col">
<EngineInstancesTable
teamIdOrSlug={props.team_slug}
instances={props.instances}
engineLinkPrefix={engineLinkPrefix}
/>
@@ -29,7 +29,10 @@ export default async function Page(props: {
loginRedirect(`/team/${params.team_slug}/~/engine`);
}
const res = await getEngineInstances({ authToken });
const res = await getEngineInstances({
authToken,
teamIdOrSlug: params.team_slug,
});
return (
<EngineInstancesList
@@ -9,8 +9,12 @@ import { RecentEngineAlertsSection } from "./RecentEngineAlerts";
export function EngineAlertsPage(props: {
instance: EngineInstance;
teamIdOrSlug: string;
}) {
const alertRulesQuery = useEngineAlertRules(props.instance.id);
const alertRulesQuery = useEngineAlertRules(
props.instance.id,
props.teamIdOrSlug,
);
const alertRules = alertRulesQuery.data ?? [];
return (
@@ -19,12 +23,14 @@ export function EngineAlertsPage(props: {
alertRules={alertRules}
engineId={props.instance.id}
alertRulesIsLoading={alertRulesQuery.isLoading}
teamIdOrSlug={props.teamIdOrSlug}
/>
<div className="h-8" />
<RecentEngineAlertsSection
alertRules={alertRules}
engineId={props.instance.id}
alertRulesIsLoading={alertRulesQuery.isLoading}
teamIdOrSlug={props.teamIdOrSlug}
/>
</div>
);
@@ -43,19 +43,23 @@ type CreateAlertMutation = UseMutationResult<
>;
export function ManageEngineAlertsSection(props: {
teamIdOrSlug: string;
alertRules: EngineAlertRule[];
alertRulesIsLoading: boolean;
engineId: string;
}) {
const notificationChannelsQuery = useEngineNotificationChannels(
props.engineId,
props.teamIdOrSlug,
);
const deleteAlertMutation = useEngineDeleteNotificationChannel(
props.engineId,
props.teamIdOrSlug,
);
const createAlertMutation = useEngineCreateNotificationChannel(
props.engineId,
props.teamIdOrSlug,
);
// not passing the mutation to avoid multiple rows sharing the same mutation state, we create the new mutation for each row instead in each component instead
@@ -23,10 +23,16 @@ export function RecentEngineAlertsSection(props: {
alertRules: EngineAlertRule[];
alertRulesIsLoading: boolean;
engineId: string;
teamIdOrSlug: string;
}) {
// TODO - pagination
// required : return the total number of alerts in response from API
const alertsQuery = useEngineAlerts(props.engineId, 100, 0);
const alertsQuery = useEngineAlerts(
props.engineId,
props.teamIdOrSlug,
100,
0,
);
const alerts = alertsQuery.data ?? [];
return (
@@ -9,5 +9,7 @@ export default async function Page(props: EngineInstancePageProps) {
teamSlug: params.team_slug,
});
return <EngineAlertsPage instance={instance} />;
return (
<EngineAlertsPage instance={instance} teamIdOrSlug={params.team_slug} />
);
}
@@ -29,7 +29,7 @@ export const EngineConfiguration: React.FC<EngineConfigurationProps> = ({
instanceUrl={instance.url}
authToken={authToken}
/>
<EngineSystem instance={instance} />
<EngineSystem instance={instance} teamIdOrSlug={teamSlug} />
</div>
);
};
@@ -6,11 +6,15 @@ import {
interface EngineSystemProps {
instance: EngineInstance;
teamIdOrSlug: string;
}
export const EngineSystem: React.FC<EngineSystemProps> = ({ instance }) => {
export const EngineSystem: React.FC<EngineSystemProps> = ({
instance,
teamIdOrSlug,
}) => {
const healthQuery = useEngineSystemHealth(instance.url);
const metricsQuery = useEngineSystemMetrics(instance.id);
const metricsQuery = useEngineSystemMetrics(instance.id, teamIdOrSlug);
if (!healthQuery.data) {
return null;
}
@@ -21,9 +25,9 @@ export const EngineSystem: React.FC<EngineSystemProps> = ({ instance }) => {
<br />
Enabled: {healthQuery.data.features?.join(", ")}
<br />
CPU: {metricsQuery.data?.data?.cpu?.toFixed(2) ?? "..."}%
CPU: {metricsQuery.data?.result?.cpu?.toFixed(2) ?? "..."}%
<br />
Memory: {metricsQuery.data?.data?.memory?.toFixed(0) ?? "..."}MB
Memory: {metricsQuery.data?.result?.memory?.toFixed(0) ?? "..."}MB
</p>
);
};
@@ -33,6 +33,7 @@ export default async function Layout(props: {
}
const instance = await getEngineInstance({
teamIdOrSlug: params.team_slug,
authToken,
engineId: params.engineId,
accountId: account.id,
@@ -23,7 +23,7 @@ export const EngineSystemMetrics: React.FC<EngineStatusProps> = ({
teamSlug,
authToken,
}) => {
const systemMetricsQuery = useEngineSystemMetrics(instance.id);
const systemMetricsQuery = useEngineSystemMetrics(instance.id, teamSlug);
const queueMetricsQuery = useEngineQueueMetrics({
authToken,
instanceUrl: instance.url,
@@ -69,8 +69,10 @@ export const EngineSystemMetrics: React.FC<EngineStatusProps> = ({
<div className="mt-10 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<Healthcheck instance={instance} />
</div>
<StatusCodes datapoints={systemMetricsQuery.data.data.statusCodes} />
<ErrorRate datapoints={systemMetricsQuery.data.data.errorRate} />
<StatusCodes
datapoints={systemMetricsQuery.data.result.statusCodes}
/>
<ErrorRate datapoints={systemMetricsQuery.data.result.errorRate} />
</div>
</Card>
);
@@ -2,6 +2,7 @@ import { API_SERVER_URL } from "@/constants/env";
import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
export async function getEngineInstance(params: {
teamIdOrSlug: string;
authToken: string;
engineId: string;
accountId: string;
@@ -19,11 +20,14 @@ export async function getEngineInstance(params: {
return sandboxEngine;
}
const res = await fetch(`${API_SERVER_URL}/v1/engine/${params.engineId}`, {
headers: {
Authorization: `Bearer ${params.authToken}`,
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${params.teamIdOrSlug}/engine/${params.engineId}`,
{
headers: {
Authorization: `Bearer ${params.authToken}`,
},
},
});
);
if (!res.ok) {
const errorMessage = await res.text();
@@ -33,8 +37,8 @@ export async function getEngineInstance(params: {
}
const json = (await res.json()) as {
data: EngineInstance;
result: EngineInstance;
};
return json.data;
return json.result;
}
@@ -20,6 +20,7 @@ export async function engineInstancePageHandler(params: {
}
const instance = await getEngineInstance({
teamIdOrSlug: params.teamSlug,
authToken,
engineId: params.engineId,
accountId: account.id,
@@ -3,12 +3,16 @@ import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
export async function getEngineInstances(params: {
authToken: string;
teamIdOrSlug: string;
}) {
const res = await fetch(`${API_SERVER_URL}/v1/engine`, {
headers: {
Authorization: `Bearer ${params.authToken}`,
const res = await fetch(
`${API_SERVER_URL}/v1/teams/${params.teamIdOrSlug}/engine`,
{
headers: {
Authorization: `Bearer ${params.authToken}`,
},
},
});
);
if (!res.ok) {
const errorMessage = await res.text();
@@ -20,12 +24,10 @@ export async function getEngineInstances(params: {
}
const json = (await res.json()) as {
data: {
instances: EngineInstance[];
};
result: EngineInstance[];
};
return {
data: json.data.instances,
data: json.result,
};
}
@@ -66,7 +66,7 @@ async function ChainDistributionCard({ data }: { data: TransactionStats[] }) {
async function ContractDistributionCard({
data,
}: { data: TransactionStats[] }) {
const reducedData = await Promise.all(
const _reducedData = await Promise.all(
Object.entries(
data
.filter((d) => d.contractAddress)
@@ -83,25 +83,36 @@ async function ContractDistributionCard({
.sort((a, b) => b[1] - a[1])
.slice(0, 10) // only top ten
.map(async ([key, value]) => {
const [chainId, contractAddress] = key.split(":");
// eslint-disable-next-line no-restricted-syntax
const chain = defineChain(Number(chainId));
const chainMeta = await getChainMetadata(chain).catch(() => undefined);
const contractData = await fetchDashboardContractMetadata(
getContract({
chain,
address: contractAddress as string, // we filter above
client: getThirdwebClient(),
}),
).catch(() => undefined);
return {
label: `${contractData?.name} (${chainMeta?.slug || chainId})`,
link: `/${chainId}/${contractAddress}`,
value,
};
try {
const [chainId, contractAddress] = key.split(":");
if (Number(chainId) === 0) {
return undefined;
}
// eslint-disable-next-line no-restricted-syntax
const chain = defineChain(Number(chainId));
const chainMeta = await getChainMetadata(chain).catch(
() => undefined,
);
const contractData = await fetchDashboardContractMetadata(
getContract({
chain,
address: contractAddress as string, // we filter above
client: getThirdwebClient(),
}),
).catch(() => undefined);
return {
label: `${contractData?.name} (${chainMeta?.slug || chainId})`,
link: `/${chainId}/${contractAddress}`,
value,
};
} catch {
return undefined;
}
}),
);
const reducedData = _reducedData.filter((d) => !!d);
const aggregateFn = () =>
new Set(
data
@@ -1,164 +0,0 @@
import { Button } from "@/components/ui/button";
import type { Range } from "components/analytics/date-range-selector";
import { Stat } from "components/analytics/stat";
import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
import { AccountAbstractionSummary } from "components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary";
import { differenceInDays } from "date-fns";
import { ArrowRightIcon, CableIcon, WalletCardsIcon } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import type {
InAppWalletStats,
UserOpStats,
WalletStats,
} from "types/analytics";
import { DateRangeSelector } from "../../../../../../components/analytics/date-range-selector";
import { IntervalSelector } from "../../../../../../components/analytics/interval-selector";
import { DailyConnectionsChartCard } from "./_components/DailyConnectionsChartCard";
import { WalletConnectorsChartCard } from "./_components/WalletConnectorsChartCard";
import { WalletDistributionChartCard } from "./_components/WalletDistributionChartCard";
export function ConnectAnalyticsDashboardUI(props: {
walletUsage: WalletStats[];
aggregateWalletUsage: WalletStats[];
aggregateUserOpUsageQuery?: UserOpStats;
connectLayoutSlug: string;
isPending: boolean;
inAppAggregateQuery?: {
allTimeStats: InAppWalletStats[];
monthlyStats: InAppWalletStats[];
};
setRange: (range: Range) => void;
range: Range;
intervalType: "day" | "week";
setIntervalType: (intervalType: "day" | "week") => void;
}) {
const { totalWallets, uniqueWallets } = useMemo(() => {
return props.aggregateWalletUsage.reduce(
(acc, curr) => {
acc.totalWallets += curr.totalConnections;
acc.uniqueWallets += curr.uniqueWalletsConnected;
return acc;
},
{ uniqueWallets: 0, totalWallets: 0 },
);
}, [props.aggregateWalletUsage]);
return (
<div className="flex flex-col gap-4 lg:gap-6">
<div>
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
Connections
</h3>
<p className="text-muted-foreground">
View how many users are connecting to your app.
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 lg:gap-6">
<Stat label="Connections" value={totalWallets} icon={CableIcon} />
<Stat
label="Unique Wallets"
value={uniqueWallets}
icon={WalletCardsIcon}
/>
</div>
</div>
<div className="h-4">
<div className="w-full border-separate border-b" />
</div>
<div>
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
In-App Wallets
</h3>
<p className="text-muted-foreground">
View metrics for your in-app wallets.
</p>
</div>
<div>
<Button asChild variant="primary" size="sm" className="w-full">
<Link href={`${props.connectLayoutSlug}/in-app-wallets`}>
In-App Wallet Metrics
<ArrowRightIcon className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
<InAppWalletsSummary
allTimeStats={props.inAppAggregateQuery?.allTimeStats}
monthlyStats={props.inAppAggregateQuery?.monthlyStats}
/>
</div>
<div className="h-4">
<div className="w-full border-separate border-b" />
</div>
<div>
<div className="mb-4 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
Account Abstraction
</h3>
<p className="text-muted-foreground">
View how smart wallets are used in your app.
</p>
</div>
<div>
<Button asChild variant="primary" size="sm" className="w-full">
<Link href={`${props.connectLayoutSlug}/account-abstraction`}>
Account Abstraction Metrics
<ArrowRightIcon className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
<AccountAbstractionSummary
aggregateUserOpUsageQuery={props.aggregateUserOpUsageQuery}
/>
</div>
<div className="h-4">
<div className="w-full border-separate border-b" />
</div>
<div className="flex justify-end gap-3">
<DateRangeSelector
range={props.range}
setRange={(newRange) => {
props.setRange(newRange);
const days = differenceInDays(newRange.to, newRange.from);
props.setIntervalType(days > 30 ? "week" : "day");
}}
/>
<IntervalSelector
intervalType={props.intervalType}
setIntervalType={props.setIntervalType}
/>
</div>
<DailyConnectionsChartCard
walletStats={props.walletUsage}
isPending={props.isPending}
/>
<WalletConnectorsChartCard
walletStats={props.walletUsage}
isPending={props.isPending}
/>
<WalletDistributionChartCard
walletStats={props.walletUsage}
isPending={props.isPending}
/>
</div>
);
}
@@ -1,46 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import { createUserOpStatsStub } from "components/smart-wallets/AccountAbstractionAnalytics/storyUtils";
import { getLastNDaysRange } from "../../../../../../../components/analytics/date-range-selector";
import { mobileViewport } from "../../../../../../../stories/utils";
import { ConnectAnalyticsDashboardUI } from "../ConnectAnalyticsDashboardUI";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Analytics Dashboard",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="container max-w-[1150px] py-8">
<ConnectAnalyticsDashboardUI
setRange={() => {}}
range={getLastNDaysRange("last-120")}
intervalType="day"
setIntervalType={() => {}}
walletUsage={createWalletStatsStub(30)}
aggregateWalletUsage={createWalletStatsStub(30)}
aggregateUserOpUsageQuery={createUserOpStatsStub(1)?.[0]}
connectLayoutSlug="connectLayoutSlug"
isPending={false}
/>
</div>
);
}
@@ -1,70 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { DailyConnectionsChartCard } from "./DailyConnectionsChartCard";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Daily Connections",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="container flex max-w-[1150px] flex-col gap-6 py-8">
<BadgeContainer label="30 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(30)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(60)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(10)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(0)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<DailyConnectionsChartCard
walletStats={createWalletStatsStub(0)}
isPending={true}
/>
</BadgeContainer>
</div>
);
}
@@ -1,243 +0,0 @@
"use client";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
EmptyChartState,
LoadingChartState,
} from "components/analytics/empty-chart-state";
import { DotNetIcon } from "components/icons/brand-icons/DotNetIcon";
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
import { UnityIcon } from "components/icons/brand-icons/UnityIcon";
import { UnrealIcon } from "components/icons/brand-icons/UnrealIcon";
import { DocLink } from "components/shared/DocLink";
import { format } from "date-fns";
import { useMemo, useState } from "react";
import {
Bar,
BarChart,
CartesianGrid,
LabelList,
XAxis,
YAxis,
} from "recharts";
import type { WalletStats } from "types/analytics";
import { formatTickerNumber } from "../../../../../../../lib/format-utils";
type ChartToShow = "uniqueWallets" | "totalWallets";
type ChartData = {
time: string; // human readable date
totalWallets: number;
uniqueWallets: number;
};
const chartConfig = {
uniqueWallets: {
label: "Unique Wallets",
color: "hsl(var(--chart-1))",
},
totalWallets: {
label: "Total Wallets",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
const chartLabelToShow: Record<ChartToShow, string> = {
uniqueWallets: "Unique Wallets",
totalWallets: "Total Wallets",
};
export function DailyConnectionsChartCard(props: {
walletStats: WalletStats[];
isPending: boolean;
}) {
const { walletStats } = props;
const [chartToShow, setChartToShow] = useState<ChartToShow>("uniqueWallets");
const chartToShowOptions: ChartToShow[] = ["uniqueWallets", "totalWallets"];
const barChartData: ChartData[] = useMemo(() => {
const chartDataMap: Map<string, ChartData> = new Map();
for (const data of walletStats) {
const chartData = chartDataMap.get(data.date);
if (!chartData) {
chartDataMap.set(data.date, {
time: format(new Date(data.date), "MMM dd"),
totalWallets: data.totalConnections,
uniqueWallets: data.uniqueWalletsConnected,
});
} else {
chartData.totalWallets += data.totalConnections;
chartData.uniqueWallets += data.uniqueWalletsConnected;
}
}
return Array.from(chartDataMap.values());
}, [walletStats]);
const disableActions = props.isPending || barChartData.length === 0;
return (
<div className="relative w-full rounded-lg border border-border bg-card p-4 md:p-6">
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
Daily Connections
</h3>
<p className="mb-3 text-muted-foreground text-sm">
Total and unique wallets addresses that connected to your app each day.
</p>
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
{/* Selector */}
<Select
onValueChange={(v) => {
setChartToShow(v as ChartToShow);
}}
value={chartToShow}
disabled={disableActions}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{chartToShowOptions.map((option) => (
<SelectItem key={option} value={option}>
{chartLabelToShow[option]}
</SelectItem>
))}
</SelectContent>
</Select>
<ExportToCSVButton
disabled={disableActions}
className="bg-background"
getData={async () => {
const header = ["Date", "Total Wallets", "Unique Wallets"];
const rows = barChartData.map((row) => [
row.time,
row.totalWallets.toString(),
row.uniqueWallets.toString(),
]);
return { header, rows };
}}
fileName="DailyConnections"
/>
</div>
{/* Chart */}
<ChartContainer
config={chartConfig}
className="h-[250px] w-full md:h-[350px]"
>
{props.isPending ? (
<LoadingChartState />
) : barChartData.length === 0 ? (
<EmptyChartState>
<div className="flex flex-col items-center justify-center">
<span className="mb-6 text-lg">
Send your first connect event
</span>
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
<DocLink
link="https://portal.thirdweb.com/typescript/v5/getting-started"
label="TypeScript"
icon={TypeScriptIcon}
/>
<DocLink
link="https://portal.thirdweb.com/react/v5"
label="React"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/react-native/v5"
label="React Native"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/dotnet/getting-started"
label="Unity"
icon={UnityIcon}
/>
<DocLink
link="https://portal.thirdweb.com/unreal-engine/getting-started"
label="Unreal Engine"
icon={UnrealIcon}
/>
<DocLink
link="https://portal.thirdweb.com/dotnet/getting-started"
label=".NET"
icon={DotNetIcon}
/>
</div>
</div>
</EmptyChartState>
) : (
<BarChart
accessibilityLayer
data={barChartData}
margin={{
top: 20,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
tickMargin={10}
axisLine={false}
/>
<YAxis
dataKey={(data) => data[chartToShow]}
tickLine={false}
axisLine={false}
tickFormatter={(value) => formatTickerNumber(value)}
/>
<ChartTooltip
cursor={true}
content={
<ChartTooltipContent
valueFormatter={(value) => formatTickerNumber(Number(value))}
/>
}
/>
<Bar
dataKey={chartToShow}
fill={`var(--color-${chartToShow})`}
radius={8}
strokeWidth={1}
className="stroke-background"
>
{barChartData.length < 50 && (
<LabelList
position="top"
offset={12}
className="invisible fill-foreground sm:visible"
fontSize={12}
formatter={(value: number) => formatTickerNumber(value)}
/>
)}
</Bar>
</BarChart>
)}
</ChartContainer>
</div>
);
}
@@ -1,70 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { WalletConnectorsChartCard } from "./WalletConnectorsChartCard";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Wallet Connectors",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<BadgeContainer label="30 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(30)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(60)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(10)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(0)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<WalletConnectorsChartCard
walletStats={createWalletStatsStub(0)}
isPending={true}
/>
</BadgeContainer>
</div>
);
}
@@ -1,260 +0,0 @@
"use client";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
EmptyChartState,
LoadingChartState,
} from "components/analytics/empty-chart-state";
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
import { DocLink } from "components/shared/DocLink";
import { format } from "date-fns";
import { useMemo, useState } from "react";
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
import type { WalletStats } from "types/analytics";
import {
formatTickerNumber,
formatWalletType,
} from "../../../../../../../lib/format-utils";
type ChartToShow = "uniqueWalletsConnected" | "totalConnections";
type ChartData = Record<string, number> & {
time: string; // human readable date
};
const chartLabelToShow: Record<ChartToShow, string> = {
uniqueWalletsConnected: "Unique Wallets",
totalConnections: "Total Wallets",
};
export function WalletConnectorsChartCard(props: {
walletStats: WalletStats[];
isPending: boolean;
}) {
// show top 10 wallets as distinct, and combine the rest as "Others"
const topWalletsToShow = 10;
const { walletStats } = props;
const [chartToShow, setChartToShow] = useState<ChartToShow>(
"uniqueWalletsConnected",
);
const chartToShowOptions: ChartToShow[] = [
"uniqueWalletsConnected",
"totalConnections",
];
const { chartConfig, chartData } = useMemo(() => {
const _chartConfig: ChartConfig = {};
const _chartDataMap: Map<string, ChartData> = new Map();
const walletTypeToValueMap: Map<string, number> = new Map();
// for each stat, add it in _chartDataMap
for (const stat of walletStats) {
const chartData = _chartDataMap.get(stat.date);
const { walletType: rawWalletType } = stat;
const walletType = formatWalletType(rawWalletType);
// if no data for current day - create new entry
if (!chartData) {
_chartDataMap.set(stat.date, {
time: format(new Date(stat.date), "MMM dd"),
[walletType]: stat[chartToShow],
} as ChartData);
} else {
chartData[walletType] =
(chartData[walletType] || 0) + stat[chartToShow];
}
walletTypeToValueMap.set(
walletType,
stat[chartToShow] + (walletTypeToValueMap.get(walletType) || 0),
);
}
const walletTypesSorted = Array.from(walletTypeToValueMap.entries())
.sort((a, b) => b[1] - a[1])
.map((w) => w[0]);
const walletTypesToShow = walletTypesSorted.slice(0, topWalletsToShow);
const walletTypesToTagAsOthers = walletTypesSorted.slice(topWalletsToShow);
// replace walletTypesToTagAsOthers walletType with "other"
for (const data of _chartDataMap.values()) {
for (const walletType in data) {
if (walletTypesToTagAsOthers.includes(walletType)) {
data.others = (data.others || 0) + (data[walletType] || 0);
delete data[walletType];
}
}
}
walletTypesToShow.forEach((walletType, i) => {
_chartConfig[walletType] = {
label: walletTypesToShow[i],
color: `hsl(var(--chart-${(i % 10) + 1}))`,
};
});
// Add Other
walletTypesToShow.push("others");
_chartConfig.others = {
label: "Others",
color: "hsl(var(--muted-foreground))",
};
return {
chartData: Array.from(_chartDataMap.values()),
chartConfig: _chartConfig,
};
}, [walletStats, chartToShow]);
const uniqueWalletTypes = Object.keys(chartConfig);
const disableActions = props.isPending || chartData.length === 0;
return (
<div className="relative w-full rounded-lg border border-border bg-card p-4 md:p-6">
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
Connected Wallets
</h3>
<p className="mb-3 text-muted-foreground text-sm">
The different types of wallets used to connect to your app each day.
</p>
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
<Select
onValueChange={(v) => {
setChartToShow(v as ChartToShow);
}}
value={chartToShow}
disabled={disableActions}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{chartToShowOptions.map((option) => (
<SelectItem key={option} value={option}>
{chartLabelToShow[option]}
</SelectItem>
))}
</SelectContent>
</Select>
<ExportToCSVButton
className="bg-background"
fileName="Connect Wallets"
disabled={disableActions}
getData={async () => {
// Shows the number of each type of wallet connected on all dates
const header = ["Date", ...uniqueWalletTypes];
const rows = chartData.map((data) => {
const { time, ...rest } = data;
return [
time,
...uniqueWalletTypes.map((w) => (rest[w] || 0).toString()),
];
});
return { header, rows };
}}
/>
</div>
{/* Chart */}
<ChartContainer config={chartConfig} className="h-[400px] w-full">
{props.isPending ? (
<LoadingChartState />
) : chartData.length === 0 ? (
<EmptyChartState>
<div className="flex flex-col items-center justify-center">
<span className="mb-6 text-lg">
Connect any wallet to your app
</span>
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
<DocLink
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
label="TypeScript"
icon={TypeScriptIcon}
/>
<DocLink
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
label="React"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
label="React Native"
icon={ReactIcon}
/>
</div>
</div>
</EmptyChartState>
) : (
<BarChart
accessibilityLayer
data={chartData}
margin={{
top: 20,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
tickMargin={10}
axisLine={false}
/>
<YAxis
dataKey={(data) =>
Object.entries(data)
.filter(([key]) => key !== "time")
.map(([, value]) => value)
.reduce((acc, current) => Number(acc) + Number(current), 0)
}
tickLine={false}
axisLine={false}
tickFormatter={(value) => formatTickerNumber(value)}
/>
<ChartTooltip
cursor={true}
content={
<ChartTooltipContent
valueFormatter={(value) => formatTickerNumber(Number(value))}
/>
}
/>
<ChartLegend content={<ChartLegendContent />} />
{uniqueWalletTypes.map((walletType) => {
return (
<Bar
key={walletType}
dataKey={walletType}
fill={chartConfig[walletType]?.color}
radius={4}
stackId="a"
strokeWidth={1}
className="stroke-background"
/>
);
})}
</BarChart>
)}
</ChartContainer>
</div>
);
}
@@ -1,70 +0,0 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
BadgeContainer,
mobileViewport,
} from "../../../../../../../stories/utils";
import { WalletDistributionChartCard } from "./WalletDistributionChartCard";
import { createWalletStatsStub } from "./storyUtils";
const meta = {
title: "Charts/Connect/Wallet Distribution",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<BadgeContainer label="30 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(30)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="60 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(60)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="10 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(10)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="0 days">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(0)}
isPending={false}
/>
</BadgeContainer>
<BadgeContainer label="Loading">
<WalletDistributionChartCard
walletStats={createWalletStatsStub(0)}
isPending={true}
/>
</BadgeContainer>
</div>
);
}
@@ -1,263 +0,0 @@
"use client";
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
EmptyChartState,
LoadingChartState,
} from "components/analytics/empty-chart-state";
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
import { DocLink } from "components/shared/DocLink";
import { useMemo, useState } from "react";
import { Pie, PieChart } from "recharts";
import type { WalletStats } from "types/analytics";
import {
formatTickerNumber,
formatWalletType,
} from "../../../../../../../lib/format-utils";
type ChartToShow = "totalConnections" | "uniqueWalletsConnected";
type ChartData = {
walletType: string;
value: number;
fill: string;
};
const chartLabelToShow: Record<ChartToShow, string> = {
uniqueWalletsConnected: "Unique Wallets",
totalConnections: "Total Wallets",
};
export function WalletDistributionChartCard(props: {
walletStats: WalletStats[];
isPending: boolean;
}) {
// show these many top wallets as distinct, and combine the rest as "Others"
const topWalletsToShow = 10;
const { walletStats } = props;
const [chartToShow, setChartToShow] =
useState<ChartToShow>("totalConnections");
const chartToShowOptions: ChartToShow[] = [
"totalConnections",
"uniqueWalletsConnected",
];
const { chartConfig, chartData, totalConnections, uniqueConnections } =
useMemo(() => {
const _chartDataMap: Map<string, number> = new Map();
const walletTypeToValueMap: Map<string, number> = new Map();
let _totalConnections = 0;
let _uniqueConnections = 0;
for (const stat of walletStats) {
const { walletType: rawWalletType } = stat;
const walletType = formatWalletType(rawWalletType);
const chartData = _chartDataMap.get(walletType);
_totalConnections += stat.totalConnections;
_uniqueConnections += stat.uniqueWalletsConnected;
_chartDataMap.set(walletType, (chartData || 0) + stat[chartToShow]);
walletTypeToValueMap.set(
walletType,
(walletTypeToValueMap.get(walletType) || 0) + stat[chartToShow],
);
}
const walletTypesSortedByValue = Array.from(
walletTypeToValueMap.entries(),
)
.sort((a, b) => b[1] - a[1])
.map((w) => w[0]);
const walletTypesToShow = walletTypesSortedByValue.slice(
0,
topWalletsToShow,
);
const walletTypesToTagAsOthers =
walletTypesSortedByValue.slice(topWalletsToShow);
for (const walletType of walletTypesToTagAsOthers) {
const val = _chartDataMap.get(walletType);
if (val) {
const othersVal = _chartDataMap.get("others");
_chartDataMap.set("others", othersVal ? othersVal + val : val);
}
_chartDataMap.delete(walletType);
}
const _chartConfig: ChartConfig = {};
walletTypesToShow.forEach((walletType, i) => {
_chartConfig[walletType] = {
label: walletTypesToShow[i],
color: `hsl(var(--chart-${(i % 10) + 1}))`,
};
});
// Add Others
_chartConfig.others = {
label: "Others",
color: "hsl(var(--muted-foreground))",
};
const _chartData: ChartData[] = Array.from(_chartDataMap).map(
([walletType, data]) => {
return {
walletType,
value: data,
fill: _chartConfig[walletType]?.color || "transparent",
};
},
);
// sort the data
_chartData.sort((a, b) => b.value - a.value);
return {
chartData: _chartData,
chartConfig: _chartConfig,
totalConnections: _totalConnections,
uniqueConnections: _uniqueConnections,
};
}, [walletStats, chartToShow]);
const disableActions = props.isPending || chartData.length === 0;
return (
<div className="relative w-full rounded-lg border border-border bg-card p-4 md:p-6">
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
Wallet Distribution
</h3>
<p className="mb-3 text-muted-foreground text-sm">
Distribution of wallet types used to connect to your app.
</p>
{/* Selector */}
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
<Select
onValueChange={(v) => {
setChartToShow(v as ChartToShow);
}}
value={chartToShow}
disabled={props.isPending || chartData.length === 0}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{chartToShowOptions.map((option) => (
<SelectItem key={option} value={option}>
{chartLabelToShow[option]}
</SelectItem>
))}
</SelectContent>
</Select>
<ExportToCSVButton
fileName="Wallet Distribution"
className="bg-background"
disabled={disableActions}
getData={async () => {
const header = [
"Wallet",
`${chartToShow === "totalConnections" ? "Total" : "Unique"} Connections`,
`Percentage of ${chartToShow === "totalConnections" ? "Total" : "Unique"} Connections`,
];
const rows = chartData.map((d) => {
return [
d.walletType,
d.value.toString(),
`${((d.value / (chartToShow === "totalConnections" ? totalConnections : uniqueConnections)) * 100).toFixed(2)}%`,
];
});
return {
header,
rows,
};
}}
/>
</div>
{/* Chart */}
<ChartContainer
config={chartConfig}
className="mt-6 h-[500px] w-full [&_.recharts-pie-label-text]:fill-foreground"
>
{props.isPending ? (
<LoadingChartState />
) : chartData.length === 0 ? (
<EmptyChartState>
<div className="flex flex-col items-center justify-center">
<span className="mb-6 text-lg">
Connect any wallet to your app
</span>
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
<DocLink
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
label="TypeScript"
icon={TypeScriptIcon}
/>
<DocLink
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
label="React"
icon={ReactIcon}
/>
<DocLink
link="https://portal.thirdweb.com/typescript/v5/supported-wallets"
label="React Native"
icon={ReactIcon}
/>
</div>
</div>
</EmptyChartState>
) : (
<PieChart>
<ChartTooltip
cursor={false}
content={
<ChartTooltipContent
valueFormatter={(v) => {
if (typeof v === "number") {
const sumValue =
chartToShow === "uniqueWalletsConnected"
? uniqueConnections
: totalConnections;
const percentageValue = ((v / sumValue) * 100).toFixed(2);
return `${percentageValue}% - ${formatTickerNumber(v)}`;
}
}}
/>
}
/>
<ChartLegend content={<ChartLegendContent />} className="" />
<Pie
data={chartData}
dataKey="value"
nameKey="walletType"
innerRadius={60}
strokeWidth={2}
stroke="hsl(var(--muted))"
/>
</PieChart>
)}
</ChartContainer>
</div>
);
}
@@ -1,52 +0,0 @@
import type { WalletId } from "thirdweb/wallets";
import type { WalletStats } from "types/analytics";
const walletsToPickFrom: WalletId[] = [
"io.metamask",
"com.binance",
"com.coinbase.wallet",
"io.zerion.wallet",
"me.rainbow",
"com.trustwallet.app",
"com.zengo",
"xyz.argent",
"com.exodus",
"app.phantom",
"com.okex.wallet",
"io.clingon",
"com.broearn",
"com.coinomi",
"com.ripio",
"com.sabay.wallet",
"io.tokoin",
"world.fncy",
"io.copiosa",
];
const pickRandomWallet = () => {
return (
walletsToPickFrom[Math.floor(Math.random() * walletsToPickFrom.length)] ||
"io.metamask"
);
};
export function createWalletStatsStub(days: number): WalletStats[] {
const stubbedData: WalletStats[] = [];
let d = days;
while (d !== 0) {
const uniqueWallets = Math.floor(Math.random() * 100);
stubbedData.push({
date: new Date(2024, 1, d).toLocaleString(),
totalConnections: uniqueWallets + Math.floor(Math.random() * 100),
uniqueWalletsConnected: uniqueWallets,
walletType: pickRandomWallet(),
});
if (Math.random() > 0.7) {
d--;
}
}
return stubbedData;
}
@@ -76,9 +76,8 @@ function Story(props: {
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
data: {
secret: new Array(86).fill("x").join(""),
secretHash: new Array(64).fill("x").join(""),
secretMasked: "123...4567",
secret: `sk_${new Array(86).fill("x").join("")}`,
secretMasked: "sk_123...4567",
},
};
}}
@@ -14,7 +14,12 @@ import { Flex, FormControl } from "@chakra-ui/react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { verifyContract } from "app/(dashboard)/(chain)/[chain_id]/[contractAddress]/sources/ContractSourcesPage";
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
import { DEFAULT_FEE_BPS, DEFAULT_FEE_RECIPIENT } from "constants/addresses";
import {
DEFAULT_FEE_BPS,
DEFAULT_FEE_BPS_NEW,
DEFAULT_FEE_RECIPIENT,
THIRDWEB_PUBLISHER_ADDRESS,
} from "constants/addresses";
import { SolidityInput } from "contract-ui/components/solidity-inputs";
import { useTrack } from "hooks/analytics/useTrack";
import { useTxNotifications } from "hooks/useTxNotifications";
@@ -161,6 +166,13 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
const constructorParams =
metadata.abi.find((a) => a.type === "constructor")?.inputs || [];
const defaultFeeRecipientFunction = metadata.abi.find(
(a) => a.type === "function" && a.name === "DEFAULT_FEE_RECIPIENT",
);
const hasInbuiltDefaultFeeConfig =
defaultFeeRecipientFunction &&
metadata.publisher === THIRDWEB_PUBLISHER_ADDRESS;
const [customFactoryNetwork, customFactoryAddress] = Object.entries(
metadata?.factoryDeploymentData?.customFactoryInput
?.customFactoryAddresses || {},
@@ -457,7 +469,9 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
name: params.contractMetadata?.name || "",
contractURI: _contractURI,
defaultAdmin: params.deployParams._defaultAdmin as string,
platformFeeBps: DEFAULT_FEE_BPS,
platformFeeBps: hasInbuiltDefaultFeeConfig
? DEFAULT_FEE_BPS_NEW
: DEFAULT_FEE_BPS,
platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
trustedForwarders: params.deployParams._trustedForwarders
? JSON.parse(params.deployParams._trustedForwarders as string)
@@ -473,8 +487,10 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
payees,
shares,
_contractURI,
_platformFeeBps: DEFAULT_FEE_BPS,
_platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
platformFeeBps: hasInbuiltDefaultFeeConfig
? DEFAULT_FEE_BPS_NEW
: DEFAULT_FEE_BPS,
platformFeeRecipient: DEFAULT_FEE_RECIPIENT,
};
const salt = params.deployDeterministic
@@ -5,13 +5,13 @@ interface PlatformFeeFieldsetProps {
isMarketplace: boolean;
}
export const PlatformFeeFieldset: React.FC<PlatformFeeFieldsetProps> = (
props,
) => {
export const PlatformFeeFieldset: React.FC<PlatformFeeFieldsetProps> = ({
isMarketplace,
}) => {
return (
<Fieldset legend="Platform fees">
<div className="flex flex-col gap-4 md:flex-row">
{props.isMarketplace ? (
{isMarketplace ? (
<p className="mb-3 pt-4 text-muted-foreground text-sm italic">
A 2.5% platform fee is deducted from each sale to support ongoing
platform operations and improvements.{" "}
@@ -285,6 +285,7 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
twAccount,
}) => {
// TODO: clean this up
const functionsWithExtension = useMemo(() => {
const allFunctions = fnsOrEvents.filter((f) => f.type === "function");
const results: ExtensionFunctions[] = [];
@@ -16,7 +16,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
import { format, fromUnixTime } from "date-fns";
import { TagIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
@@ -24,17 +23,6 @@ import { toast } from "sonner";
import { useActiveAccount } from "thirdweb/react";
import { z } from "zod";
const LazyShareFreeWalletsModal = dynamic(
() =>
import("./share-free-wallets-modal.client").then((mod) => ({
default: mod.ShareFreeWalletsModal,
})),
{
ssr: false,
loading: () => null,
},
);
export type ActiveCouponResponse = {
id: string;
start: number;
@@ -49,10 +37,7 @@ export type ActiveCouponResponse = {
function ApplyCouponCard(props: {
teamId: string | undefined;
onCouponApplied: (
data: ActiveCouponResponse,
isFreeWalletsCoupon: boolean,
) => void;
onCouponApplied: (data: ActiveCouponResponse) => void;
isPaymentSetup: boolean;
}) {
const searchParams = useSearchParams();
@@ -102,9 +87,7 @@ export function ApplyCouponCardUI(props: {
status: number;
data: null | ActiveCouponResponse;
}>;
onCouponApplied:
| ((data: ActiveCouponResponse, isFreeWalletsCoupon: boolean) => void)
| undefined;
onCouponApplied: ((data: ActiveCouponResponse) => void) | undefined;
prefillPromoCode?: string;
scrollIntoView?: boolean;
isPaymentSetup: boolean;
@@ -145,14 +128,7 @@ export function ApplyCouponCardUI(props: {
case 200: {
toast.success("Coupon applied successfully");
if (res.data) {
props.onCouponApplied?.(
res.data,
// prod & dev
values.promoCode === "FREEWALLETS" ||
// new prod code
values.promoCode === "FREEWALLETS24" ||
values.promoCode === "TESTFREEWALLETS",
);
props.onCouponApplied?.(res.data);
}
break;
}
@@ -284,7 +260,6 @@ export function CouponSection(props: {
isPaymentSetup: boolean;
}) {
const address = useActiveAccount()?.address;
const [showShareModal, setShowShareModal] = useState(false);
const [optimisticCouponData, setOptimisticCouponData] = useState<
| {
type: "added";
@@ -358,15 +333,12 @@ export function CouponSection(props: {
<Suspense fallback={<LoadingCouponSection />}>
<ApplyCouponCard
teamId={props.teamId}
onCouponApplied={(coupon, isFreeWalletsCoupon) => {
onCouponApplied={(coupon) => {
setOptimisticCouponData({
type: "added",
data: coupon,
});
if (isFreeWalletsCoupon) {
setShowShareModal(true);
}
activeCoupon.refetch().then(() => {
setOptimisticCouponData(undefined);
});
@@ -375,13 +347,6 @@ export function CouponSection(props: {
/>
</Suspense>
)}
{showShareModal && (
<LazyShareFreeWalletsModal
isOpen={showShareModal}
onOpenChange={setShowShareModal}
/>
)}
</>
);
}
@@ -1,401 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { zodResolver } from "@hookform/resolvers/zod";
import { ChevronLeftIcon, MailIcon } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { DynamicHeight } from "../../../../@/components/ui/DynamicHeight";
import { XIcon } from "../../../icons/brand-icons/XIcon";
export function ShareFreeWalletsModal(props: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}) {
const [screen, setScreen] = useState<"base" | "email">("base");
return (
<Dialog open={props.isOpen} onOpenChange={props.onOpenChange}>
<DialogContent className="p-0 md:max-w-[380px]">
<DynamicHeight>
{screen === "base" && (
<div className="fade-in-0 animate-in duration-300">
<DialogHeader className="p-6 text-center">
<div className="mb-4 flex justify-center gap-2">
<VibrantSmileIcon className="size-16" />
</div>
<DialogTitle className="mb-1 text-lg">
Congratulations!
</DialogTitle>
<DialogDescription className="text-balance text-base text-muted-foreground">
You are getting free wallets for the next 12 months
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-3 border-border border-t p-6">
<Button variant="outline" className="gap-2" asChild>
<TrackedLinkTW
href={`https://x.com/intent/post?text=${encodeURIComponent(twitterPostBody)}`}
target="_blank"
category="share-free-wallets"
label="twitter-share"
>
<XIcon className="size-4" />
Share on X
</TrackedLinkTW>
</Button>
<Button
variant="primary"
className="gap-2"
onClick={() => setScreen("email")}
>
<MailIcon className="size-4 shrink-0" />
Share via Email
</Button>
</div>
</div>
)}
{screen === "email" && (
<SendEmailScreen goBack={() => setScreen("base")} />
)}
</DynamicHeight>
</DialogContent>
</Dialog>
);
}
const emailFormSchema = z.object({
email: z.string().email(),
app: z.string(),
});
function SendEmailScreen(props: {
goBack: () => void;
}) {
const form = useForm<z.infer<typeof emailFormSchema>>({
resolver: zodResolver(emailFormSchema),
defaultValues: {
email: "",
app: "default",
},
});
return (
<div className="fade-in-0 animate-in duration-300">
<Form {...form}>
<form onSubmit={form.handleSubmit(() => {})}>
<div className="p-6">
<DialogHeader className="text-center">
<div className="mb-4 flex justify-center gap-2">
<VibrantEmailIcon className="size-16" />
</div>
<DialogTitle className="mb-1 text-lg">
Share via Email
</DialogTitle>
<DialogDescription className="text-balance text-base text-muted-foreground">
Give the gift of unlimited wallets to another dev
</DialogDescription>
</DialogHeader>
<div className="h-4" />
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder="[email protected]"
{...field}
type="email"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="h-4" />
<FormField
control={form.control}
name="app"
render={({ field }) => (
<FormItem>
<FormLabel>Share with</FormLabel>
<FormControl>
<Select
{...field}
onValueChange={(value) => form.setValue("app", value)}
>
<SelectTrigger>
<SelectValue placeholder="Select Email App" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="default">
Default Email App
</SelectItem>
<SelectItem value="gmail">Gmail</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex justify-between gap-3 border-border border-t p-6">
<Button variant="outline" className="gap-2" onClick={props.goBack}>
<ChevronLeftIcon className="size-4" />
Back
</Button>
{!form.formState.isValid ? (
<Button variant="primary" className="gap-2" type="submit">
<MailIcon className="size-4 shrink-0" />
Share via Email
</Button>
) : (
<Button variant="primary" className="gap-2" asChild>
<TrackedLinkTW
href={
form.watch("app") === "gmail"
? openGmailEmailApp(form.watch("email"))
: openDefaultEmailApp(form.watch("email"))
}
target="_blank"
category="share-free-wallets"
label="email-share"
>
<MailIcon className="size-4 shrink-0" />
Share via Email
</TrackedLinkTW>
</Button>
)}
</div>
</form>
</Form>
</div>
);
}
function openDefaultEmailApp(email: string) {
return `mailto:${email}?subject=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailPostBody)}`;
}
function openGmailEmailApp(email: string) {
return `https://mail.google.com/mail/?view=cm&fs=1&to=${email}&su=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailPostBody)}`;
}
const twitterPostBody = `\
Just claimed the free, unlimited wallets offer from @thirdweb! 🚀
No more per-wallet pricing, full support for web, mobile, and console.
Here's to building without constraints ✨
👉 https://thirdweb.com/unlimited-wallets 👈`;
const emailSubject = "You've unlocked Unlimited Wallets!";
const emailPostBody = `\
Hey there,
I just came across something that might interest you. thirdweb is offering free, unlimited wallets for developers.
I've claimed it and thought you might want to take a look too.
Here's the lowdown:
* It's completely free
* No more per-wallet pricing constraints
* Full support for web, mobile, and console development
I'm pretty excited about the possibilities this opens up for building without limitations. You can check it out here: https://thirdweb.com/unlimited-wallets
Let me know what you think if you decide to give it a try!`;
function VibrantSmileIcon(props: {
className?: string;
}) {
return (
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<path
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
fill="url(#paint0_linear_754_5630)"
fillOpacity="0.08"
/>
<path
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
stroke="url(#paint1_linear_754_5630)"
strokeWidth="2"
/>
<path
d="M26.2857 34.8567C26.2857 34.8567 28.4286 37.7139 32 37.7139C35.5714 37.7139 37.7143 34.8567 37.7143 34.8567M36.2857 27.7139H36.3M27.7143 27.7139H27.7286M46.2857 31.9996C46.2857 39.8894 39.8898 46.2853 32 46.2853C24.1102 46.2853 17.7143 39.8894 17.7143 31.9996C17.7143 24.1098 24.1102 17.7139 32 17.7139C39.8898 17.7139 46.2857 24.1098 46.2857 31.9996ZM37 27.7139C37 28.1084 36.6802 28.4282 36.2857 28.4282C35.8912 28.4282 35.5714 28.1084 35.5714 27.7139C35.5714 27.3194 35.8912 26.9996 36.2857 26.9996C36.6802 26.9996 37 27.3194 37 27.7139ZM28.4286 27.7139C28.4286 28.1084 28.1088 28.4282 27.7143 28.4282C27.3198 28.4282 27 28.1084 27 27.7139C27 27.3194 27.3198 26.9996 27.7143 26.9996C28.1088 26.9996 28.4286 27.3194 28.4286 27.7139Z"
stroke="url(#paint2_linear_754_5630)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<defs>
<linearGradient
id="paint0_linear_754_5630"
x1="-4.34726"
y1="20.5361"
x2="61.0409"
y2="66.8729"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint1_linear_754_5630"
x1="-4.34726"
y1="20.5361"
x2="61.0409"
y2="66.8729"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint2_linear_754_5630"
x1="15.7735"
y1="26.8818"
x2="44.9647"
y2="47.5678"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
</defs>
</svg>
);
}
function VibrantEmailIcon(props: {
className?: string;
}) {
return (
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={props.className}
role="presentation"
>
<path
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
fill="url(#paint0_linear_754_9605)"
fillOpacity="0.08"
/>
<path
d="M1 32C1 14.8792 14.8792 1 32 1C49.1208 1 63 14.8792 63 32C63 49.1208 49.1208 63 32 63C14.8792 63 1 49.1208 1 32Z"
stroke="url(#paint1_linear_754_9605)"
strokeWidth="2"
/>
<path
d="M17.7144 24.857L29.3785 33.0219C30.3231 33.6831 30.7953 34.0137 31.309 34.1417C31.7628 34.2548 32.2374 34.2548 32.6911 34.1417C33.2048 34.0137 33.6771 33.6831 34.6216 33.0219L46.2858 24.857M24.5715 43.4284H39.4286C41.8289 43.4284 43.029 43.4284 43.9457 42.9613C44.7522 42.5504 45.4078 41.8948 45.8187 41.0884C46.2858 40.1716 46.2858 38.9715 46.2858 36.5713V27.4284C46.2858 25.0282 46.2858 23.8281 45.8187 22.9113C45.4078 22.1049 44.7522 21.4493 43.9457 21.0384C43.029 20.5713 41.8289 20.5713 39.4286 20.5713H24.5715C22.1713 20.5713 20.9712 20.5713 20.0544 21.0384C19.248 21.4493 18.5924 22.1049 18.1815 22.9113C17.7144 23.8281 17.7144 25.0282 17.7144 27.4284V36.5713C17.7144 38.9715 17.7144 40.1716 18.1815 41.0884C18.5924 41.8948 19.248 42.5504 20.0544 42.9613C20.9712 43.4284 22.1713 43.4284 24.5715 43.4284Z"
stroke="url(#paint2_linear_754_9605)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<defs>
<linearGradient
id="paint0_linear_754_9605"
x1="-4.34726"
y1="20.5361"
x2="61.0409"
y2="66.8729"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint1_linear_754_9605"
x1="-4.34726"
y1="20.5361"
x2="61.0409"
y2="66.8729"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
<linearGradient
id="paint2_linear_754_9605"
x1="15.7736"
y1="27.9056"
x2="40.3444"
y2="49.6705"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F213A4" />
<stop offset="0.1517" stopColor="#E011A7" />
<stop offset="0.4554" stopColor="#B20DAF" />
<stop offset="0.8789" stopColor="#6806BB" />
<stop offset="1" stopColor="#5204BF" />
</linearGradient>
</defs>
</svg>
);
}
@@ -1,37 +0,0 @@
import { Button } from "@/components/ui/button";
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { mobileViewport } from "../../../../stories/utils";
import { ShareFreeWalletsModal } from "./share-free-wallets-modal.client";
const meta = {
title: "Billing/Coupons/ShareFreeWalletsModal",
component: Variants,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Variants>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Variants() {
const [isOpen, setIsOpen] = useState(true);
return (
<div className="flex grow items-center justify-center p-6">
<Button onClick={() => setIsOpen(true)}>Open Again</Button>
<ShareFreeWalletsModal isOpen={isOpen} onOpenChange={setIsOpen} />
</div>
);
}
@@ -1,7 +1,11 @@
export const THIRDWEB_DEPLOYER_ADDRESS =
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
export const THIRDWEB_PUBLISHER_ADDRESS =
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
export const DEFAULT_FEE_RECIPIENT =
"0x1af20c6b23373350ad464700b5965ce4b0d2ad94";
export const DEFAULT_FEE_BPS = 250;
export const DEFAULT_FEE_BPS_NEW = 150;
-11
View File
@@ -5,14 +5,3 @@ const compactNumberFormatter = new Intl.NumberFormat("en-US", {
export const formatTickerNumber = (value: number) => {
return compactNumberFormatter.format(value);
};
export const formatWalletType = (walletType: string) => {
return walletType.toLowerCase().includes("inapp") ||
walletType.toLowerCase().includes("embedded")
? "in-app"
: walletType.toLowerCase().includes("metamask")
? "metamask"
: walletType.toLowerCase().includes("coinbase")
? "coinbase"
: walletType.toLowerCase();
};
+5 -5
View File
@@ -23,8 +23,8 @@
"@radix-ui/react-tooltip": "1.1.8",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jose": "6.0.7",
"lucide-react": "0.476.0",
"jose": "6.0.8",
"lucide-react": "0.477.0",
"next": "15.2.0",
"next-themes": "^0.4.4",
"react": "19.0.0",
@@ -37,13 +37,13 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.0",
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"eslint": "^9",
"eslint": "9.21.0",
"eslint-config-next": "15.2.0",
"postcss": "8.5.3",
"tailwindcss": "3.4.17",
"typescript": "5.7.3"
"typescript": "5.8.2"
}
}
+6 -6
View File
@@ -31,12 +31,12 @@
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "1.1.8",
"@tanstack/react-query": "5.66.9",
"@tanstack/react-query": "5.67.1",
"@thirdweb-dev/engine": "^0.0.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jose": "6.0.7",
"lucide-react": "0.476.0",
"jose": "6.0.8",
"lucide-react": "0.477.0",
"next": "15.2.0",
"next-themes": "^0.4.4",
"nextjs-toploader": "^1.6.12",
@@ -55,15 +55,15 @@
"zod": "3.24.2"
},
"devDependencies": {
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"eslint": "8.57.0",
"eslint-config-next": "15.2.0",
"eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
"postcss": "8.5.3",
"tailwindcss": "3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.7.3"
"typescript": "5.8.2"
}
}
@@ -113,8 +113,8 @@ function NoFundsPopup() {
<CodeExample
preview={<PayTransactionButtonPreview />}
code={`import { trasnfer } from "thirdweb/extensions/erc1155";
import { PayEmbed, useActiveAccount } from "thirdweb/react";
code={`import { transfer } from "thirdweb/extensions/erc1155";
import { TransactionButton, useActiveAccount } from "thirdweb/react";
function App() {
@@ -157,13 +157,7 @@ export function BlueprintPlaygroundUI(props: {
const defaultValues = useMemo(() => {
const values: Record<string, string | number> = {};
for (const param of parameters) {
if (param.name === "chain") {
values.chain = "1";
} else if (
param.schema &&
"type" in param.schema &&
param.schema.default
) {
if (param.schema && "type" in param.schema && param.schema.default) {
values[param.name] = param.schema.default;
} else {
values[param.name] = "";
@@ -3,24 +3,21 @@ import type { ConnectButtonProps } from "thirdweb/react";
import { ecosystemWallet } from "thirdweb/wallets";
import { StyledConnectEmbed } from "../styled-connect-embed";
const getEcosystem = () => {
const getEcosystemWallet = () => {
if (
process.env.NEXT_PUBLIC_IN_APP_WALLET_URL?.endsWith(".thirdweb-dev.com")
) {
// dev ecosystem
return "ecosystem.catlovers";
return ecosystemWallet("ecosystem.catlovers");
}
// prod ecosystem
return "ecosystem.thirdweb-engs";
return ecosystemWallet("ecosystem.thirdweb-engs", {
partnerId: "50fd8850-3c6c-48dd-969c-622f88b52d95",
});
};
export function EcosystemConnectEmbed(
props?: Omit<ConnectButtonProps, "client" | "theme">,
) {
return (
<StyledConnectEmbed
{...props}
wallets={[ecosystemWallet(getEcosystem())]}
/>
);
return <StyledConnectEmbed {...props} wallets={[getEcosystemWallet()]} />;
}
@@ -35,7 +35,7 @@ export function PayTransactionPreview() {
const { theme } = useTheme();
const { data: nft } = useReadContract(getNFT, {
contract: nftContract,
tokenId: 1n,
tokenId: 2n,
});
return (
@@ -51,7 +51,7 @@ export function PayTransactionPreview() {
transaction: claimTo({
contract: nftContract,
quantity: 1n,
tokenId: 1n,
tokenId: 2n,
to: account?.address || "",
}),
metadata: nft?.metadata,
+5 -5
View File
@@ -27,7 +27,7 @@
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@tanstack/react-query": "5.66.9",
"@tanstack/react-query": "5.67.1",
"@tryghost/content-api": "^1.11.21",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -35,7 +35,7 @@
"flexsearch": "^0.7.43",
"github-slugger": "^2.0.0",
"he": "^1.2.0",
"lucide-react": "0.476.0",
"lucide-react": "0.477.0",
"next": "15.2.0",
"nextjs-toploader": "^1.6.12",
"node-html-markdown": "^1.3.0",
@@ -59,10 +59,10 @@
"@types/flexsearch": "^0.7.6",
"@types/he": "^1.2.3",
"@types/mdx": "^2.0.13",
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/react-html-parser": "^2.0.6",
"@types/react-html-parser": "^2.0.7",
"@types/tryghost__content-api": "^1.3.16",
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
@@ -77,6 +77,6 @@
"postcss": "8.5.3",
"tailwindcss": "3.4.17",
"tsx": "4.19.3",
"typescript": "5.7.3"
"typescript": "5.8.2"
}
}
+28 -26
View File
@@ -1,40 +1,39 @@
import { createMetadata } from "@doc";
import { createMetadata, Details } from "@doc";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - FAQs",
title: "thirdweb Universal Bridge - FAQs",
icon: "thirdweb",
},
title: "thirdweb Pay - FAQs - thirdweb",
description: "thirdweb Pay - Frequently Asked Questions",
title: "thirdweb Universal Bridge - FAQs - thirdweb",
description: "thirdweb Universal Bridge - Frequently Asked Questions",
});
# FAQs
### What are the fees for Buy With Fiat?
<Details summary="What are the fees for Buy With Fiat?">
thirdweb takes a 1% fee on all Buy With Fiat transactions. Additional processing fees will depend on the selected onramp vendor.
</Details>
### What are the fees for Buy With Crypto?
<Details summary="What are the fees for Buy With Crypto?">
thirdweb takes a 1% fee on all Buy With Crypto transactions.
</Details>
### How does fee sharing work for Buy With Crypto?
<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.
</Details>
### What does the term “network fees” refer to?
<Details summary="What does the term “network fees” refer to?">
Network fees, also referred to as miner fees, are paid to the miner for processing crypto transactions and securing the respective network. These fees do not go to thirdweb.
</Details>
### What are your transaction limits for Buy With Fiat?
<Details summary="What are your transaction limits for Buy With Fiat?">
We currently require a $1 minimum purchase for both stablecoin and non-stablecoin purchases through Buy With Fiat.
Our transaction maximum starts from $1500 per week for new users and can increase over time to $10,000 per week.
</Details>
### What countries does Buy With Fiat support?
<Details summary="What countries does Buy With Fiat support?">
**Buy With Fiat** is available 130+ countries. The following countries are _UNSUPPORTED_:
- Afghanistan
@@ -60,9 +59,9 @@ Our transaction maximum starts from $1500 per week for new users and can increas
- Yemen
All United States are supported, excluding Hawaii.
</Details>
### Does thirdweb offer direct onramping to tokens through Buy With Fiat?
<Details summary="Does thirdweb offer direct onramping to tokens through Buy With Fiat?">
We offer direct onramping **internationally** to the following tokens:
- Mainnet (Ethereum)
@@ -80,19 +79,22 @@ We offer direct onramping to the following tokens in the **US only**:
All other tokens will require an additional Buy With Crypto step.
\* Not available in New York or the EU.
</Details>
### How does KYC work with Buy With Fiat
<Details summary="How does KYC work with Buy With Fiat">
Users will be required to complete KYC verification for their first purchase using 'Buy with Fiat.' After that, no further KYC will be needed for future purchases on the same platform.
</Details>
### What cryptocurrencies does Pay support?
<Details summary="What cryptocurrencies does Pay support?">
Pay can support any currency on our [supported chains](/connect/pay/supported-chains).
</Details>
### How are cryptocurrency prices set in thirdweb Pay?
<Details summary="How are cryptocurrency prices set in thirdweb Pay?">
Prices are set depending on the liquidity provider used to route your transaction.
</Details>
### Does thirdweb Pay offer fiat payouts?
<Details summary="Does thirdweb Pay offer fiat payouts?">
Fiat payouts are not currently available.
</Details>
@@ -10,12 +10,12 @@ import GetStartedSend from "../assets/get-started-send.png";
import GetStartedEmbed from "../assets/get-started-embed.png";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay - Get Started",
title: "thirdweb Universal Bridge - Get Started",
icon: "thirdweb",
},
title: "thirdweb Pay - Buy With Fiat - thirdweb",
title: "Universal Bridge Implementation Guide — thirdweb docs",
description:
"Get Started with Pay - Onchain Purchases Directly From Credit or Debit Card",
"Learn how to implement all-in-one web3 payment solution, Universal bridge: the technical step-by-step process",
});
# Get Started
@@ -1,19 +1,20 @@
import { createMetadata, DocImage, Grid, SDKCard, FeatureCard } from "@doc";
import PayOverviewImage from "../assets/pay-overview.png";
import SupportedChains from "../../../_images/supported-chains.png";
export const metadata = createMetadata({
image: {
title: "thirdweb Pay",
title: "thirdweb Universal Bridge",
icon: "thirdweb",
},
title: "thirdweb Pay - thirdweb",
title: "thirdweb Universal Bridge Docs: Web3 Payments, On-ramping, bridging & swapping",
description:
"thirdweb Pay - Fast, flexible credit card and crypto point of sale",
"Learn everything about thirdweb’s web3 payments solution, Universal Bridge. Technical docs on onramping, bridging + swapping.",
});
# Overview
Pay allows your users to purchase cryptocurrencies and execute transactions with their credit card or debit card, or with any token 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.
<DocImage src={PayOverviewImage} />
@@ -67,14 +68,30 @@ Pay allows your users to purchase cryptocurrencies and execute transactions with
</div>
### Use Cases
## Supported Chains
- **Onramp funds directly into any transaction,** perfect for any of the following use cases:
- **Games** selling in-game assets onchain.
- **DEXes** looking to help users deposit funds.
- **Staking apps** onboarding new users to their pools.
- **Social apps** looking to add tipping or other paid functionality to their application.
- **Non-profits** accepting donations in crypto.
Universal Bridge is supported on select EVM compatible chain. To view the full list, visit [thirdweb chainlist](https://thirdweb.com/chainlist?service=pay).
<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 > Pay in the thirdweb dashboard.
## 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).
## Integration Options
@@ -6,8 +6,8 @@ export const metadata = createMetadata({
title: "thirdweb Pay - Test Mode",
icon: "thirdweb",
},
title: "thirdweb Pay - Test Mode - thirdweb",
description: "thirdweb Pay - Test Mode",
title: "Enable Test Mode for Universal bridge — thirdweb docs",
description: "Learn how to enable test mode for web3 payments with the thirdweb Universal bridge feature in our technical docs",
});
# Enable Test Mode
@@ -2,9 +2,9 @@ import { createMetadata, DocImage } from "@doc";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
export const metadata = createMetadata({
title: "Webhooks | thirdweb Pay",
title: "Universal Bridge Webhooks Implementation Guide — thirdweb Docs",
description:
"Configure Pay webhooks to get notified at any point during a transaction's lifecycle.",
"Learn how to set up webhooks for web3 payments with thirdweb’s Universal bridge feature: the technical docs",
});
# Webhooks
+60 -81
View File
@@ -270,99 +270,78 @@ export const sidebar: SideBar = {
// Blockchain API
// TODO Overview page?
{
name: "Onchain Interactions",
name: "Universal Bridge",
isCollapsible: false,
links: [
// Pay
{
name: "Bridge & Swap",
name: "Overview",
href: `${paySlug}/overview`,
},
{
name: "Get Started",
href: `${paySlug}/get-started`,
expanded: true,
links: [
{
name: "Overview",
href: `${paySlug}/overview`,
name: "ConnectButton",
href: `${paySlug}/get-started#option-1-connectbutton`,
},
{
name: "Get Started",
href: `${paySlug}/get-started`,
expanded: true,
links: [
{
name: "ConnectButton",
href: `${paySlug}/get-started#option-1-connectbutton`,
},
{
name: "Embed Pay",
href: `${paySlug}/get-started#option-2-embed-pay`,
},
{
name: "Send a Transaction",
href: `${paySlug}/get-started#option-3-send-a-transaction-with-pay`,
},
],
name: "Embed Pay",
href: `${paySlug}/get-started#option-2-embed-pay`,
},
{
name: "Supported Chains",
href: `${paySlug}/supported-chains`,
},
{
name: "Onramp Providers",
href: `${paySlug}/onramp-providers`,
},
{
name: "Fee Sharing",
href: `${paySlug}/fee-sharing`,
},
{
name: "Webhooks",
href: `${paySlug}/webhooks`,
},
{
name: "Testing Pay",
href: `${paySlug}/testing-pay`,
},
{
name: "Guides",
isCollapsible: true,
links: [
{
name: "Accept Direct Payments",
href: `${paySlug}/guides/accept-direct-payments`,
},
{
name: "Build a Custom Experience",
href: `${paySlug}/guides/build-a-custom-experience`,
},
],
},
{
name: "Customization",
isCollapsible: true,
links: [
{
name: "ConnectButton",
href: `${paySlug}/customization/connectbutton`,
},
{
name: "PayEmbed",
href: `${paySlug}/customization/payembed`,
},
{
name: "useSendTransaction",
href: `${paySlug}/customization/send-transaction`,
},
],
},
{
name: "FAQs",
href: `${paySlug}/faqs`,
name: "Send a Transaction",
href: `${paySlug}/get-started#option-3-send-a-transaction-with-pay`,
},
],
},
{
name: "Webhooks",
href: `${paySlug}/webhooks`,
},
{
name: "Testing Pay",
href: `${paySlug}/testing-pay`,
},
{
name: "Guides",
isCollapsible: true,
links: [
{
name: "Accept Direct Payments",
href: `${paySlug}/guides/accept-direct-payments`,
},
{
name: "Build a Custom Experience",
href: `${paySlug}/guides/build-a-custom-experience`,
},
],
},
{
name: "Customization",
isCollapsible: true,
links: [
{
name: "ConnectButton",
href: `${paySlug}/customization/connectbutton`,
},
{
name: "PayEmbed",
href: `${paySlug}/customization/payembed`,
},
{
name: "useSendTransaction",
href: `${paySlug}/customization/send-transaction`,
},
],
},
{
name: "FAQs",
href: `${paySlug}/faqs`,
},
],
},
{ separator: true },
+5 -5
View File
@@ -15,11 +15,11 @@
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-query": "5.66.9",
"@tanstack/react-query": "5.67.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"lucide-react": "0.476.0",
"lucide-react": "0.477.0",
"next": "15.2.0",
"next-themes": "^0.4.4",
"react": "19.0.0",
@@ -35,18 +35,18 @@
},
"devDependencies": {
"@next/eslint-plugin-next": "15.2.0",
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
"knip": "5.45.0",
"postcss": "8.5.3",
"postcss-load-config": "^6.0.1",
"tailwindcss": "3.4.17",
"typescript": "5.7.3"
"typescript": "5.8.2"
}
}
+2 -2
View File
@@ -43,9 +43,9 @@
"@changesets/changelog-github": "0.5.1",
"@changesets/cli": "2.28.1",
"@manypkg/get-packages": "2.2.2",
"@types/node": "22.13.5",
"@types/node": "22.13.9",
"turbo": "2.4.4",
"typescript": "5.7.3"
"typescript": "5.8.2"
},
"pnpm": {
"overrides": {
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/service-utils
## 0.8.13
### Patch Changes
- [#6416](https://github.com/thirdweb-dev/js/pull/6416) [`44bb003`](https://github.com/thirdweb-dev/js/commit/44bb0034a69334f05cfa14132d56e354536e77ff) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] Add "pay" as a usage source
## 0.8.12
### Patch Changes
- [#6409](https://github.com/thirdweb-dev/js/pull/6409) [`67354dc`](https://github.com/thirdweb-dev/js/commit/67354dc848bfb00d2296d5e52990321e9d28abb0) Thanks [@arcoraven](https://github.com/arcoraven)! - [service-utils] remove unneeded cloudflare worker types
## 0.8.11
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/service-utils",
"version": "0.8.11",
"version": "0.8.13",
"type": "module",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
@@ -51,8 +51,8 @@
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250224.0",
"@types/node": "22.13.5",
"typescript": "5.7.3",
"@types/node": "22.13.9",
"typescript": "5.8.2",
"vitest": "3.0.7"
},
"scripts": {
@@ -1,9 +1,9 @@
import type {
ExecutionContext,
KVNamespace,
Request,
Response,
} from "@cloudflare/workers-types";
import type { Request } from "@cloudflare/workers-types";
import type { CoreServiceConfig, TeamAndProjectResponse } from "../core/api.js";
import { authorize } from "../core/authorize/index.js";
import type {
@@ -1,4 +1,4 @@
import { Headers, type Request, fetch } from "@cloudflare/workers-types";
import type { Request } from "@cloudflare/workers-types";
import type { CoreAuthInput } from "../core/types.js";
import type {
ClientUsageV2Event,
@@ -34,7 +34,7 @@ export async function sendUsageV2Events<T extends UsageV2Options>(
// Forward headers from the origin request.
// Determine endpoint and auth header based on provided credentials.
let url: string;
const headers = new Headers(authInput.req.headers);
const headers = new Headers(Object.fromEntries(authInput.req.headers));
headers.set("Content-Type", "application/json");
if (serviceKey) {
// If a service key is provided, call the non-public usage endpoint.
@@ -7,6 +7,7 @@ export const USAGE_V2_SOURCES = [
"sdk",
"storage",
"wallet",
"pay",
] as const;
export type UsageV2Source = (typeof USAGE_V2_SOURCES)[number];
export function getTopicName(source: UsageV2Source) {
+14
View File
@@ -1,5 +1,19 @@
# thirdweb
## 5.90.3
### Patch Changes
- [#6424](https://github.com/thirdweb-dev/js/pull/6424) [`38ae2d3`](https://github.com/thirdweb-dev/js/commit/38ae2d3c6b7d5052974e8a61333997699230ad45) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Expose getUserOpHash utility function
## 5.90.2
### Patch Changes
- [#6415](https://github.com/thirdweb-dev/js/pull/6415) [`fb446bd`](https://github.com/thirdweb-dev/js/commit/fb446bd0993a0f431fbc67e6142ec20866c4bc65) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - emit onPurchaseSuccess for wallet to wallet transfer flow before execution
- [#6412](https://github.com/thirdweb-dev/js/pull/6412) [`9f1a77f`](https://github.com/thirdweb-dev/js/commit/9f1a77f4c49350018dad0037c4ab675818057264) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Allow resending OTP on failures
## 5.90.1
### Patch Changes
+12 -12
View File
@@ -1,6 +1,6 @@
{
"name": "thirdweb",
"version": "5.90.1",
"version": "5.90.3",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
@@ -218,7 +218,7 @@
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-tooltip": "1.1.8",
"@tanstack/react-query": "5.66.9",
"@tanstack/react-query": "5.67.1",
"@walletconnect/ethereum-provider": "2.17.5",
"@walletconnect/sign-client": "2.17.5",
"abitype": "1.0.8",
@@ -334,19 +334,19 @@
"@aws-sdk/client-lambda": "3.592.0",
"@aws-sdk/credential-providers": "3.592.0",
"@biomejs/biome": "1.9.4",
"@chromatic-com/storybook": "3.2.4",
"@chromatic-com/storybook": "3.2.5",
"@codspeed/vitest-plugin": "4.0.0",
"@coinbase/wallet-mobile-sdk": "1.1.2",
"@mobile-wallet-protocol/client": "0.1.2",
"@react-native-async-storage/async-storage": "2.1.1",
"@size-limit/preset-big-lib": "11.2.0",
"@storybook/addon-essentials": "8.6.0",
"@storybook/addon-interactions": "8.6.0",
"@storybook/addon-links": "8.6.0",
"@storybook/addon-onboarding": "8.6.0",
"@storybook/react": "8.6.0",
"@storybook/react-vite": "8.6.0",
"@storybook/test": "8.6.0",
"@storybook/addon-essentials": "8.6.3",
"@storybook/addon-interactions": "8.6.3",
"@storybook/addon-links": "8.6.3",
"@storybook/addon-onboarding": "8.6.3",
"@storybook/react": "8.6.3",
"@storybook/react-vite": "8.6.3",
"@storybook/test": "8.6.3",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
@@ -375,10 +375,10 @@
"rimraf": "6.0.1",
"sharp": "^0.33.5",
"size-limit": "11.2.0",
"storybook": "8.6.0",
"storybook": "8.6.3",
"typedoc": "0.27.9",
"typedoc-better-json": "0.9.4",
"typescript": "5.7.3",
"typescript": "5.8.2",
"vite": "6.2.0",
"vitest": "3.0.7"
}
@@ -6,7 +6,7 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
MarketplaceV3: "0x8b0DBCf5b7D01eBB0F24525CE8AB72F16CE4F8C8",
},
[324]: {
MarketplaceV3: "0xBc02441a36Bb4029Cd191b20243c2e41B862F118",
MarketplaceV3: "0x88e7F2bEf0a23073b7Cc2c79D307Dbd9138Fa79c",
},
[11124]: {
MarketplaceV3: "0x2dA4Dd326A6482679547071be21f74685d730504",
@@ -18,7 +18,7 @@ export const ZKSYNC_IMPLEMENTATIONS: Record<number, Record<string, string>> = {
MarketplaceV3: "0x9EB0830B0b10010F2a53383517A7D0B75531Bb1b",
},
[2741]: {
MarketplaceV3: "0x4027561E163a420c4e5Db46E07EBd581CAa8Bb62",
MarketplaceV3: "0x4b14569c7B79DBe686Ac3Ba5996131E7EDaB7a93",
},
};
@@ -5,6 +5,7 @@ export {
createUnsignedUserOp,
signUserOp,
createAndSignUserOp,
getUserOpHash,
} from "../../wallets/smart/lib/userop.js";
export {
@@ -17,10 +17,8 @@ import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value
import { toEther } from "../../utils/units.js";
import { generateMerkleTreeInfoERC1155 } from "../airdrop/write/merkleInfoERC1155.js";
import { name } from "../common/read/name.js";
import { deployERC20Contract } from "../prebuilts/deploy-erc20.js";
import { deployERC1155Contract } from "../prebuilts/deploy-erc1155.js";
import { balanceOf } from "./__generated__/IERC1155/read/balanceOf.js";
import { totalSupply } from "./__generated__/IERC1155/read/totalSupply.js";
import { nextTokenIdToMint } from "./__generated__/IERC1155Enumerable/read/nextTokenIdToMint.js";
import { canClaim } from "./drops/read/canClaim.js";
import { getActiveClaimCondition } from "./drops/read/getActiveClaimCondition.js";
@@ -472,63 +470,6 @@ describe.runIf(process.env.TW_SECRET_KEY)(
expect(isGetNFTsSupported(selectors)).toBe(true);
});
/**
* This is to document the behavior where one can claim without paying if the claiming address
* is the same as the PrimaryRecipientAddress, because of this Solidity code:
* ```solidity
* // CurrencyTransferLib.sol
* function safeTransferERC20(address _currency, address _from, address _to, uint256 _amount) internal {
* if (_from == _to) {
* return;
* }
* ...
* }
* ```
*/
it("address that is the same with PrimaryFeeRecipient can claim without paying ERC20", async () => {
const tokenAddress = await deployERC20Contract({
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
account: TEST_ACCOUNT_C,
type: "TokenERC20",
params: {
name: "token20",
contractURI: TEST_CONTRACT_URI,
},
});
const tokenId = 5n;
const setClaimTx = setClaimConditions({
contract,
tokenId,
phases: [
{
maxClaimableSupply: 100n,
maxClaimablePerWallet: 100n,
currencyAddress: tokenAddress,
price: 1000,
startTime: new Date(),
},
],
});
await sendAndConfirmTransaction({
transaction: setClaimTx,
account: TEST_ACCOUNT_C,
});
const transaction = claimTo({
contract,
tokenId,
quantity: 50n,
to: TEST_ACCOUNT_C.address,
});
await sendAndConfirmTransaction({
transaction,
account: TEST_ACCOUNT_C,
});
const supplyCount = await totalSupply({ contract, id: tokenId });
expect(supplyCount).toBe(50n);
});
it("getActiveClaimCondition should work", async () => {
// Create a public allowlist claim phase
const snapshot = [
@@ -6,12 +6,18 @@ import type { ThirdwebClient } from "../../../../../../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js";
import { getContract } from "../../../../../../../contract/contract.js";
import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js";
import {
type GetCurrencyMetadataResult,
getCurrencyMetadata,
} from "../../../../../../../extensions/erc20/read/getCurrencyMetadata.js";
import { approve } from "../../../../../../../extensions/erc20/write/approve.js";
import { transfer } from "../../../../../../../extensions/erc20/write/transfer.js";
import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js";
import { getBuyWithCryptoTransfer } from "../../../../../../../pay/buyWithCrypto/getTransfer.js";
import { sendAndConfirmTransaction } from "../../../../../../../transaction/actions/send-and-confirm-transaction.js";
import { sendTransaction } from "../../../../../../../transaction/actions/send-transaction.js";
import { prepareTransaction } from "../../../../../../../transaction/prepare-transaction.js";
import type { TransactionReceipt } from "../../../../../../../transaction/types.js";
import type { Address } from "../../../../../../../utils/address.js";
import { toWei } from "../../../../../../../utils/units.js";
import { iconSize } from "../../../../../../core/design-system/index.js";
@@ -42,6 +48,7 @@ type TransferConfirmationScreenProps = {
tokenAmount: string;
transactionMode?: boolean;
payOptions?: PayUIOptions;
onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined;
};
export function TransferConfirmationScreen(
@@ -202,10 +209,33 @@ export function TransferConfirmationScreen(
to: receiverAddress,
amount: tokenAmount,
});
await sendAndConfirmTransaction({
account: props.payer.account,
transaction,
});
const [txResult, tokenMetadata] = await Promise.all([
sendAndConfirmTransaction({
account: props.payer.account,
transaction,
}),
getCurrencyMetadata({
contract: getContract({
address: isNativeToken(token)
? NATIVE_TOKEN_ADDRESS
: token.address,
chain: chain,
client: client,
}),
}),
]);
// its the last step before the transaction, so propagate onPurchaseSuccess here
props.onSuccess?.(
transferBuyWithCryptoQuote({
token,
chain,
tokenMetadata,
tokenAmount,
fromAddress: payer.account.address,
toAddress: receiverAddress,
transaction: txResult,
}),
);
// switch to execute step
setStep("execute");
setStatus({ id: "idle" });
@@ -299,3 +329,85 @@ export function TransferConfirmationScreen(
</Container>
);
}
function transferBuyWithCryptoQuote(args: {
token: ERC20OrNativeToken;
chain: Chain;
tokenMetadata: GetCurrencyMetadataResult;
tokenAmount: string;
fromAddress: string;
toAddress: string;
transaction: TransactionReceipt;
}): BuyWithCryptoStatus {
const {
token,
chain,
tokenMetadata,
tokenAmount,
fromAddress,
toAddress,
transaction,
} = args;
return {
status: "COMPLETED",
subStatus: "SUCCESS",
swapType: "TRANSFER",
quote: {
createdAt: new Date().toISOString(),
fromToken: {
chainId: chain.id,
tokenAddress: isNativeToken(token)
? NATIVE_TOKEN_ADDRESS
: token.address,
decimals: tokenMetadata.decimals,
symbol: tokenMetadata.symbol,
name: tokenMetadata.name,
priceUSDCents: 0,
},
toToken: {
chainId: chain.id,
tokenAddress: isNativeToken(token)
? NATIVE_TOKEN_ADDRESS
: token.address,
decimals: tokenMetadata.decimals,
symbol: tokenMetadata.symbol,
name: tokenMetadata.name,
priceUSDCents: 0,
},
fromAmountWei: toWei(tokenAmount).toString(),
fromAmount: tokenAmount,
toAmountWei: toWei(tokenAmount).toString(),
toAmount: tokenAmount,
toAmountMin: tokenAmount,
toAmountMinWei: toWei(tokenAmount).toString(),
estimated: {
feesUSDCents: 0,
gasCostUSDCents: 0,
slippageBPS: 0,
toAmountMinUSDCents: 0,
toAmountUSDCents: 0,
fromAmountUSDCents: 0,
durationSeconds: 0,
},
},
fromAddress,
toAddress,
source: {
transactionHash: transaction.transactionHash,
amount: tokenAmount,
amountWei: toWei(tokenAmount).toString(),
amountUSDCents: 0,
completedAt: new Date().toISOString(),
token: {
chainId: chain.id,
tokenAddress: isNativeToken(token)
? NATIVE_TOKEN_ADDRESS
: token.address,
decimals: tokenMetadata.decimals,
symbol: tokenMetadata.symbol,
name: tokenMetadata.name,
priceUSDCents: 0,
},
},
};
}
@@ -276,7 +276,7 @@ export function OTPLoginUI(props: {
{!isWideModal && <Line />}
<Container p={isWideModal ? undefined : "lg"}>
<Container p={isWideModal ? undefined : "lg"} gap="xs">
{accountStatus === "error" && (
<Text size="sm" center color="danger">
{locale.emailLoginScreen.failedToSendCode}
@@ -297,7 +297,7 @@ export function OTPLoginUI(props: {
</Container>
)}
{accountStatus === "sent" && (
{accountStatus !== "sending" && (
<LinkButton onClick={sendEmailOrSms} type="button">
{locale.emailLoginScreen.resendCode}
</LinkButton>
@@ -74,16 +74,12 @@ describe.runIf(process.env.TW_SECRET_KEY)("prepareengineTransaction", () => {
const { message, messageType, signature } = result;
expect(isHex(signature)).toBe(true);
expect(message).toMatchInlineSnapshot(`
{
"data": "0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"gas": 64340n,
"nonce": 0n,
"to": "${erc20Contract.address}",
"value": 0n,
}
`);
expect(message.data).toBe(
"0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
);
expect(message.from).toBe(TEST_ACCOUNT_A.address);
expect(message.to).toBe(erc20Contract.address);
expect(message.value).toBe(0n);
expect(messageType).toMatchInlineSnapshot(`"forward"`);
});
@@ -72,16 +72,12 @@ describe.runIf(process.env.TW_SECRET_KEY)(
const { message, messageType, signature } = result;
expect(isHex(signature)).toBe(true);
expect(message).toMatchInlineSnapshot(`
{
"data": "0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"gas": 64340n,
"nonce": 0n,
"to": "${erc20Contract.address}",
"value": 0n,
}
`);
expect(message.from).toBe(TEST_ACCOUNT_A.address);
expect(message.to).toBe(erc20Contract.address);
expect(message.value).toBe(0n);
expect(message.data).toBe(
"0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000",
);
expect(messageType).toMatchInlineSnapshot(`"forward"`);
});
},
@@ -27,7 +27,8 @@ export async function isZkSyncChain(chain: Chain) {
chain.id === 2741 ||
chain.id === 240 ||
chain.id === 555271 ||
chain.id === 61166
chain.id === 61166 ||
chain.id === 555272
) {
return true;
}
@@ -20,54 +20,57 @@ import { getContract } from "../../contract/contract.js";
import { deployContract } from "../../contract/deployment/deploy-with-abi.js";
import { resolveImplementation } from "./resolveImplementation.js";
describe("Resolve implementation", async () => {
it("should extract implementation address for minimal proxy contract", async () => {
const resolved = resolveImplementation(NFT_DROP_CONTRACT);
expect((await resolved).address).to.equal(NFT_DROP_IMPLEMENTATION);
});
it("should extract implementation address for matic proxy contract", async () => {
const resolved = resolveImplementation(POLYGON_USDT_PROXY_CONTRACT);
expect((await resolved).address).to.equal(
POLYGON_USDT_IMPLEMENTATION.toLowerCase(),
);
});
it("should extract implementation address for base USDC proxy contract", async () => {
const resolved = resolveImplementation(BASE_USDC_PROXY_CONTRACT);
expect((await resolved).address).to.equal(
BASE_USDC_IMPLEMENTATION.toLowerCase(),
);
});
it("should extract implementation address for ERC1967 proxy contract", async () => {
const implementationAddress = await deployContract({
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
account: TEST_ACCOUNT_A,
bytecode: DUMMY_BYTECODE,
abi: [],
describe.runIf(process.env.TW_SECRET_KEY)(
"Resolve implementation",
async () => {
it("should extract implementation address for minimal proxy contract", async () => {
const resolved = resolveImplementation(NFT_DROP_CONTRACT);
expect((await resolved).address).to.equal(NFT_DROP_IMPLEMENTATION);
});
const proxyAddress = await deployContract({
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
account: TEST_ACCOUNT_A,
bytecode: ERC1967_PROXY_BYTECODE,
abi: ERC1967_PROXY_CONSTRUCTOR_ABI as Abi,
constructorParams: {
logic: implementationAddress,
data: "0x",
},
it("should extract implementation address for matic proxy contract", async () => {
const resolved = resolveImplementation(POLYGON_USDT_PROXY_CONTRACT);
expect((await resolved).address).to.equal(
POLYGON_USDT_IMPLEMENTATION.toLowerCase(),
);
});
const proxy = getContract({
chain: ANVIL_CHAIN,
address: proxyAddress,
client: TEST_CLIENT,
it("should extract implementation address for base USDC proxy contract", async () => {
const resolved = resolveImplementation(BASE_USDC_PROXY_CONTRACT);
expect((await resolved).address).to.equal(
BASE_USDC_IMPLEMENTATION.toLowerCase(),
);
});
const resolved = await resolveImplementation(proxy);
expect(resolved.address).to.equal(implementationAddress);
});
});
it("should extract implementation address for ERC1967 proxy contract", async () => {
const implementationAddress = await deployContract({
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
account: TEST_ACCOUNT_A,
bytecode: DUMMY_BYTECODE,
abi: [],
});
const proxyAddress = await deployContract({
client: TEST_CLIENT,
chain: ANVIL_CHAIN,
account: TEST_ACCOUNT_A,
bytecode: ERC1967_PROXY_BYTECODE,
abi: ERC1967_PROXY_CONSTRUCTOR_ABI as Abi,
constructorParams: {
logic: implementationAddress,
data: "0x",
},
});
const proxy = getContract({
chain: ANVIL_CHAIN,
address: proxyAddress,
client: TEST_CLIENT,
});
const resolved = await resolveImplementation(proxy);
expect(resolved.address).to.equal(implementationAddress);
});
},
);
@@ -10,6 +10,7 @@
export async function resolvePromisedValue<V>(
value: V,
): Promise<V extends () => Promise<infer R> ? R : V> {
// @ts-expect-error - this works fine, but TS doesn't like it since 5.8
return typeof value === "function" ? await value() : value;
}
@@ -6,13 +6,15 @@ import type { ClientScopedStorage } from "../authentication/client-scoped-storag
export async function signMessage({
client,
payload: { message, isRaw },
payload: { message, isRaw, originalMessage, chainId },
storage,
}: {
client: ThirdwebClient;
payload: {
message: string;
isRaw: boolean;
originalMessage?: string;
chainId?: number;
};
storage: ClientScopedStorage;
}) {
@@ -37,6 +39,8 @@ export async function signMessage({
messagePayload: {
message,
isRaw,
originalMessage,
chainId,
},
}),
},
@@ -217,10 +217,10 @@ export class EnclaveWallet implements IWebWallet {
return { transactionHash };
},
async signMessage({ message }) {
async signMessage({ message, originalMessage, chainId }) {
const messagePayload = (() => {
if (typeof message === "string") {
return { message, isRaw: false };
return { message, isRaw: false, originalMessage, chainId };
}
return {
message:
@@ -228,6 +228,8 @@ export class EnclaveWallet implements IWebWallet {
? message.raw
: bytesToHex(message.raw),
isRaw: true,
originalMessage,
chainId,
};
})();
@@ -182,7 +182,15 @@ export type Account = {
* const signature = await account.signMessage({ message: 'hello!' });
* ```
*/
signMessage: ({ message }: { message: SignableMessage }) => Promise<Hex>;
signMessage: ({
message,
originalMessage,
chainId,
}: {
message: SignableMessage;
originalMessage?: string;
chainId?: number;
}) => Promise<Hex>;
/**
* Sign the given typed data and return the signature
* @example
@@ -179,6 +179,7 @@ export async function estimateUserOpGasCost(args: {
client: args.client,
}),
),
waitForDeployment: false,
});
let gasLimit = 0n;
@@ -1,5 +1,5 @@
import { maxUint96 } from "ox/Solidity";
import { concat, keccak256, toHex } from "viem";
import { concat } from "viem";
import type { Chain } from "../../../chains/types.js";
import type { ThirdwebClient } from "../../../client/client.js";
import {
@@ -16,9 +16,11 @@ import type { PreparedTransaction } from "../../../transaction/prepare-transacti
import type { TransactionReceipt } from "../../../transaction/types.js";
import { encodeAbiParameters } from "../../../utils/abi/encodeAbiParameters.js";
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
import type { Hex } from "../../../utils/encoding/hex.js";
import { type Hex, toHex } from "../../../utils/encoding/hex.js";
import { hexToBytes } from "../../../utils/encoding/to-bytes.js";
import { isThirdwebUrl } from "../../../utils/fetch.js";
import { keccak256 } from "../../../utils/hashing/keccak256.js";
import { stringify } from "../../../utils/json.js";
import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-value.js";
import type { Account } from "../../interfaces/wallet.js";
import type {
@@ -321,7 +323,9 @@ async function populateUserOp_v0_7(args: {
createAccountOverride: overrides?.createAccount,
}),
);
markAccountDeploying(accountContract);
if (waitForDeployment) {
markAccountDeploying(accountContract);
}
}
const partialOp: UserOperationV07 = {
@@ -481,7 +485,9 @@ async function populateUserOp_v0_6(args: {
accountSalt: overrides?.accountSalt,
createAccountOverride: overrides?.createAccount,
});
markAccountDeploying(accountContract);
if (waitForDeployment) {
markAccountDeploying(accountContract);
}
}
const partialOp: UserOperationV06 = {
@@ -593,6 +599,54 @@ export async function signUserOp(args: {
}): Promise<UserOperationV06 | UserOperationV07> {
const { userOp, chain, entrypointAddress, adminAccount } = args;
const userOpHash = await getUserOpHash({
client: args.client,
userOp,
chain,
entrypointAddress,
});
if (adminAccount.signMessage) {
const signature = await adminAccount.signMessage({
message: {
raw: hexToBytes(userOpHash),
},
originalMessage: stringify(userOp),
chainId: chain.id,
});
return {
...userOp,
signature,
};
}
throw new Error("signMessage not implemented in signingAccount");
}
/**
* Get the hash of a user operation.
* @param args - The options for getting the user operation hash
* @returns - The user operation hash
* @example
* ```ts
* import { getUserOpHash } from "thirdweb/wallets/smart";
*
* const userOp = await createUnsignedUserOp(...);
* const userOpHash = await getUserOpHash({
* client,
* userOp,
* chain,
* });
* ```
* @walletUtils
*/
export async function getUserOpHash(args: {
client: ThirdwebClient;
userOp: UserOperationV06 | UserOperationV07;
chain: Chain;
entrypointAddress?: string;
}): Promise<Hex> {
const { userOp, chain, entrypointAddress } = args;
const entrypointVersion = getEntryPointVersion(
entrypointAddress || ENTRYPOINT_ADDRESS_v0_6,
);
@@ -619,19 +673,7 @@ export async function signUserOp(args: {
userOp: userOp as UserOperationV06,
});
}
if (adminAccount.signMessage) {
const signature = await adminAccount.signMessage({
message: {
raw: hexToBytes(userOpHash),
},
});
return {
...userOp,
signature,
};
}
throw new Error("signMessage not implemented in signingAccount");
return userOpHash;
}
async function getAccountInitCode(options: {
@@ -49,9 +49,10 @@ const contract = getContract({
address: "0xe2cb0eb5147b42095c2FfA6F7ec953bb0bE347D8",
});
describe.sequential(
describe.runIf(process.env.TW_SECRET_KEY).sequential(
"SmartWallet 0.7 core tests",
{
retry: 0,
timeout: 240_000,
},
() => {
@@ -200,7 +201,7 @@ describe.sequential(
});
await expect(tx).rejects.toMatchInlineSnapshot(`
[TransactionError: Error - Not authorized
[TransactionError: Execution Reverted: {"code":3,"message":"execution reverted: Not authorized"}
contract: ${contract.address}
chainId: 11155111]
@@ -52,6 +52,7 @@ const factoryAddress = "0x564cf6453a1b0FF8DB603E92EA4BbD410dea45F3"; // pre 712
describe.runIf(process.env.TW_SECRET_KEY).sequential(
"SmartWallet core tests",
{
retry: 0,
timeout: 240_000,
},
() => {
@@ -28,7 +28,7 @@ const client = TEST_CLIENT;
const DEFAULT_FACTORY_ADDRESS = "0xB1846E893CA01c5Dcdaa40371C1e13f2e0Df5717";
const DEFAULT_VALIDATOR_ADDRESS = "0x7D3631d823e0De311DC86f580946EeF2eEC81fba";
describe.sequential(
describe.runIf(process.env.TW_SECRET_KEY).sequential(
"SmartWallet modular tests",
{
retry: 0,
+4
View File
@@ -1,5 +1,9 @@
# @thirdweb-dev/wagmi-adapter
## 0.2.27
## 0.2.26
## 0.2.25
## 0.2.24
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/wagmi-adapter",
"version": "0.2.25",
"version": "0.2.27",
"repository": {
"type": "git",
"url": "git+https://github.com/thirdweb-dev/js.git#main"
+2132 -2059
View File
File diff suppressed because it is too large Load Diff