Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
47354d5a19 Version Packages (#8243)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-14 12:29:02 -07:00
Joaquim VergesandGitHub 8280efb991 [SDK] Fix Coinbase wallet transaction desktop popup for mobile QR login (#8253) 2025-10-14 12:20:50 -07:00
MananTank 5f11ae0cd4 Dashboard: Show Purchase data in bridge tx page, add tx status card in project>bridge page (#8251)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the transaction status viewing capabilities in the application. It introduces new components and modifies existing ones to improve user experience and functionality related to transaction data.

### Detailed summary
- Added `ViewTxStatus` component for viewing transaction status.
- Updated `Page` component to include `ViewTxStatus` and adjust layout.
- Improved error handling for transaction retrieval.
- Introduced `WithPurchaseData` story for `BridgeStatus`.
- Enhanced `BridgeStatus` to display purchase data if available.
- Modified `RouteDiscovery` to streamline form submission and error handling.

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

<!-- end pr-codex -->

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

* **New Features**
  * Added a “View Transaction Status” tool with chain selector, hash validation, and link to results in a new tab.
  * Transaction page shows a “Purchase Data” section with formatted JSON when available.

* **Bug Fixes**
  * More graceful handling for missing/invalid transactions/receipts with an inline “Transaction not found” message.

* **UI**
  * Reworked Route Discovery into an inline form with clearer error display and updated spacing.

* **Tests**
  * Added a Storybook variant showcasing purchase data on the Completed status.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 18:37:48 +00:00
d4mr c9a0c23c6c [dashboard] add solana policies to vault access token (#8239)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on adding new metadata patterns and required metadata patterns for various transaction types related to `solana`, enhancing the functionality of the `vault.client.ts` file.

### Detailed summary
- Added new `metadataPatterns` for `solana:read`, `solana:create`, `solana:signTransaction`, and `solana:signMessage` types.
- Included `requiredMetadataPatterns` for `solana:create` type.
- Each pattern includes `projectId`, `teamId`, and a fixed `type` of `"server-wallet"`.

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

<!-- end pr-codex -->

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

## Summary by CodeRabbit

* **New Features**
  * Added Solana-specific permissions to access tokens.
  * Wallet tokens now support solana:read, solana:create, solana:signTransaction, and solana:signMessage.
  * Management tokens now support solana:read and solana:create.
  * Permissions can be scoped via metadata (team, project, server-wallet) for finer control.
  * No changes to existing flows or error handling; new scopes are available during token creation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-14 18:06:44 +00:00
MananTank 43b0680124 [MNY-242] Dashboard: Remove animated background from bridge page (#8219)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on code cleanup and improvements in the `apps/dashboard` project. It removes unused components and makes adjustments to existing components for better styling and functionality.

### Detailed summary
- Deleted `DarkVeil` and `BridgeVeil` components.
- Adjusted the styling of the `PageHeader` component.
- Modified button size and styles in `BuyAndSwapEmbed`.
- Updated `BridgePage` component layout and header styles.
- Improved `DataPill` component styles.
- Refined `DotsBackgroundPattern` visibility and styles.
- Updated dependency versions in `package.json` and `pnpm-lock.yaml`.

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

<!-- end pr-codex -->

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

* **Style**
  * Removed animated veil background and simplified header visuals; adjusted responsive typography, spacing, borders, paddings, and colors for improved light/dark contrast.
  * Added a small dot icon beside the first pill on mobile, improved pill layout and muted pill text color on larger screens.
  * Made tab buttons smaller and refined active-state styling.

* **Refactor**
  * Removed legacy veil background components to simplify the UI.

* **Chores**
  * Removed an unused graphics dependency from the dashboard.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-13 21:44:06 +00:00
MananTank cccbde9a2e [MNY-240] Dashboard: Show bridge status info on transaction page if available (#8241)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new tabbed interface for bridge status and overview details in the transaction view. It enhances user experience by allowing easy navigation between different states of a bridge transaction.

### Detailed summary
- Added `BridgeAndOverviewTabs` component for tab navigation.
- Implemented `BridgeStatus` component to display bridge transaction status.
- Created stories for different bridge statuses (`Completed`, `Pending`, `Failed`, `NotFound`).
- Enhanced `Page` component to include bridge status with polling.
- Refactored transaction details layout for better clarity.

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

<!-- end pr-codex -->
2025-10-13 21:28:49 +00:00
MananTank 85a6afc377 [MNY-249] Dashboard: /<chain>/tx/<tx-hash> page UI improvements (#8234)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `page.tsx` component for transaction details in the dashboard, improving its layout and user experience by introducing new components and restructuring sections for better clarity and functionality.

### Detailed summary
- Added `Breadcrumb` for navigation.
- Replaced static text with `CopyTextButton` for transaction hash, from, and to addresses.
- Introduced `GridItem` component for consistent layout of transaction details.
- Enhanced transaction details display with tooltips and icons.
- Improved formatting of timestamps and values.
- Added sections for transaction fee, gas prices, and burnt fees.
- Included input data display using `PlainTextCodeBlock`.

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

<!-- end pr-codex -->
2025-10-13 21:28:49 +00:00
MananTank e74aca9c90 [MNY-239] Dashboard: Show webhook sends for Bridge webhooks (#8213)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the payment and webhook functionalities in the dashboard application by adding `authToken` support across various components and improving the webhook management interface.

### Detailed summary
- Added `authToken` prop to multiple components including `PayAnalytics`, `PaymentHistory`, and `QuickStartSection`.
- Enhanced `getWebhooks`, `createWebhook`, `deleteWebhook`, and `updateWebhook` functions to accept `authToken`.
- Improved the webhook management UI with `WebhookCard` and related functionalities.
- Added dialog components for creating and editing webhooks with validation.
- Updated API calls to include `authToken` for authentication in requests.

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

<!-- end pr-codex -->

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

- **New Features**
  - Card-based webhooks UI with create/edit modal, per-card actions, delete confirmation, webhook detail page, and webhook sends viewer (history, filtering, pagination, resend).
  - Payments area: payment links and payments viewer wired to authenticated CRUD and paginated payments list; resend and webhook-send actions supported.

- **Enhancements**
  - authToken propagated across webhooks, payments, payment links, fees, and related components to enable authenticated operations.
  - Webhook objects no longer expose secret fields; create/update/delete now return usable payloads.

- **Documentation**
  - Storybook stories added for webhook sends UI (loading, results, error, resend scenarios).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-13 21:28:49 +00:00
MananTank b0cac940bd Dashboard: Add block_timestamp_from param to useTokenTransfers (#8121)
<!--

## 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 enhances the `useTokenTransfers` functionality by adding a time filter for token transfers and improving the refetching logic based on error states.

### Detailed summary
- Introduced a constant `THIRTY_DAYS_AGO` to represent the timestamp 30 days prior.
- Added a search parameter `block_timestamp_from` to the `url` with the calculated timestamp.
- Changed `refetchInterval` to conditionally return `false` if there's an error in the data.

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

<!-- end pr-codex -->

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

* **New Features**
  * Token transfer history now defaults to the last 30 days for more relevant results.

* **Bug Fixes**
  * Auto-refresh pauses when an error occurs, preventing repeated failures and unnecessary requests.
  * Auto-refresh resumes normal operation once the error clears, keeping data up to date without manual refresh.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-13 20:20:30 +00:00
MananTank 1a19e14845 SDK: Fix getNFT tests, Playground: Fix build (#8242)
<!--

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

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

## Notes for the reviewer

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

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on updating the `tokenAddress` format in the tests and modifying the instantiation of the `thirdwebAI` and `thirdwebClient` to use a consistent `SECRET_KEY` variable for better readability and maintainability.

### Detailed summary
- Updated `tokenAddress` format in `getNFT.test.ts` and `getNFTs.test.ts`.
- Changed `thirdwebAI` instantiation in `route.ts` to use `SECRET_KEY` variable.
- Modified `thirdwebClient` instantiation in `paywall/route.ts` to use `SECRET_KEY` variable.
- Maintained existing environment variable usage for wallet addresses and API URLs.

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

<!-- end pr-codex -->

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

* **New Features**
  * Added a new public GET paywall endpoint.

* **Tests**
  * Updated NFT test snapshots to correct token address casing.
  * Removed an obsolete single-line test comment.

* **Chores**
  * API handlers now initialize needed services per request and read required environment secrets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-13 19:58:30 +00:00
YashandGitHub 464af6a54c Stylus CLI updates (#8235) 2025-10-14 00:11:33 +07:00
46 changed files with 2871 additions and 906 deletions
-1
View File
@@ -47,7 +47,6 @@
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
"nuqs": "^2.4.3",
"ogl": "^1.0.11",
"papaparse": "^5.5.3",
"pluralize": "^8.0.0",
"posthog-js": "1.256.1",
@@ -1,28 +1,28 @@
"use server";
import type { Address } from "thirdweb";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
const UB_BASE_URL = NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST;
type Webhook = {
export type Webhook = {
url: string;
label: string;
active: boolean;
createdAt: string;
id: string;
secret: string;
version?: number; // TODO (UB) make this mandatory after migration
};
export async function getWebhooks(props: { clientId: string; teamId: string }) {
const authToken = await getAuthToken();
export async function getWebhooks(params: {
clientId: string;
teamId: string;
authToken: string;
}) {
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "GET",
});
@@ -36,28 +36,55 @@ export async function getWebhooks(props: { clientId: string; teamId: string }) {
return json.data as Array<Webhook>;
}
export async function createWebhook(props: {
export async function getWebhookById(params: {
clientId: string;
teamId: string;
authToken: string;
webhookId: string;
}) {
const res = await fetch(
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
{
headers: {
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "GET",
},
);
if (!res.ok) {
const text = await res.text();
throw new Error(text);
}
const json = await res.json();
return json.data as Webhook;
}
export async function createWebhook(params: {
clientId: string;
teamId: string;
version?: number;
url: string;
label: string;
secret?: string;
authToken: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
body: JSON.stringify({
label: props.label,
secret: props.secret,
url: props.url,
version: props.version,
label: params.label,
secret: params.secret,
url: params.url,
version: params.version,
}),
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "POST",
});
@@ -67,23 +94,23 @@ export async function createWebhook(props: {
throw new Error(text);
}
return;
return (await res.json()) as Webhook;
}
export async function deleteWebhook(props: {
export async function deleteWebhook(params: {
clientId: string;
teamId: string;
webhookId: string;
authToken: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(
`${UB_BASE_URL}/v1/developer/webhooks/${props.webhookId}`,
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "DELETE",
},
@@ -94,7 +121,151 @@ export async function deleteWebhook(props: {
throw new Error(text);
}
return;
return true;
}
export async function updateWebhook(params: {
clientId: string;
teamId: string;
webhookId: string;
authToken: string;
body: {
version?: number;
url: string;
label: string;
};
}) {
const res = await fetch(
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
{
method: "PUT",
body: JSON.stringify(params.body),
headers: {
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
},
);
if (!res.ok) {
const text = await res.text();
throw new Error(text);
}
const json = await res.json();
return json.data as Webhook;
}
export type WebhookSend = {
id: string;
webhookId: string;
webhookUrl: string;
createdAt: string;
onrampId: string | null;
paymentId: string | null;
response: string | null;
responseStatus: number;
status: "PENDING" | "COMPLETED" | "FAILED";
success: boolean;
transactionId: string | null;
body: unknown;
};
type WebhookSendsResponse = {
data: WebhookSend[];
pagination: {
limit: number;
offset: number;
total: number;
};
};
export async function getWebhookSends(options: {
authToken: string;
projectClientId: string;
teamId: string;
limit?: number;
offset?: number;
webhookId: string;
success?: boolean;
}): Promise<WebhookSendsResponse> {
const { limit, offset, success, webhookId, authToken } = options;
const url = new URL(
`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/developer/webhook-sends`,
);
url.searchParams.set("webhookId", webhookId);
if (limit !== undefined) {
url.searchParams.set("limit", limit.toString());
}
if (offset !== undefined) {
url.searchParams.set("offset", offset.toString());
}
if (success !== undefined) {
url.searchParams.set("success", success.toString());
}
const response = await fetch(url.toString(), {
method: "GET",
headers: {
"x-client-id": options.projectClientId,
"x-team-id": options.teamId,
Authorization: `Bearer ${authToken}`,
},
});
if (!response.ok) {
const errorJson = await response.json();
throw new Error(errorJson.message);
}
return (await response.json()) as WebhookSendsResponse;
}
export async function resendWebhook(
params: {
authToken: string;
projectClientId: string;
teamId: string;
} & (
| {
paymentId: string;
}
| {
onrampId: string;
}
),
) {
const url = new URL(
`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/developer/webhooks/retry`,
);
const response = await fetch(url.toString(), {
method: "POST",
body: JSON.stringify(
"paymentId" in params
? { paymentId: params.paymentId }
: { onrampId: params.onrampId },
),
headers: {
"Content-Type": "application/json",
"x-client-id": params.projectClientId,
"x-team-id": params.teamId,
Authorization: `Bearer ${params.authToken}`,
},
});
if (!response.ok) {
const errorJson = await response.json();
throw new Error(errorJson.message);
}
return true;
}
type PaymentLink = {
@@ -116,17 +287,17 @@ type PaymentLink = {
amount: bigint;
};
export async function getPaymentLinks(props: {
export async function getPaymentLinks(params: {
clientId: string;
teamId: string;
authToken: string;
}): Promise<Array<PaymentLink>> {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "GET",
});
@@ -159,7 +330,7 @@ export async function getPaymentLinks(props: {
}));
}
export async function createPaymentLink(props: {
export async function createPaymentLink(params: {
clientId: string;
teamId: string;
title: string;
@@ -171,26 +342,25 @@ export async function createPaymentLink(props: {
amount: bigint;
purchaseData?: unknown;
};
authToken: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
body: JSON.stringify({
title: props.title,
imageUrl: props.imageUrl,
title: params.title,
imageUrl: params.imageUrl,
intent: {
destinationChainId: props.intent.destinationChainId,
destinationTokenAddress: props.intent.destinationTokenAddress,
receiver: props.intent.receiver,
amount: props.intent.amount.toString(),
purchaseData: props.intent.purchaseData,
destinationChainId: params.intent.destinationChainId,
destinationTokenAddress: params.intent.destinationTokenAddress,
receiver: params.intent.receiver,
amount: params.intent.amount.toString(),
purchaseData: params.intent.purchaseData,
},
}),
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "POST",
});
@@ -207,20 +377,20 @@ export async function createPaymentLink(props: {
return response.data;
}
export async function deletePaymentLink(props: {
export async function deletePaymentLink(params: {
clientId: string;
teamId: string;
paymentLinkId: string;
authToken: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(
`${UB_BASE_URL}/v1/developer/links/${props.paymentLinkId}`,
`${UB_BASE_URL}/v1/developer/links/${encodeURIComponent(params.paymentLinkId)}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "DELETE",
},
@@ -231,7 +401,7 @@ export async function deletePaymentLink(props: {
throw new Error(text);
}
return;
return true;
}
export type Fee = {
@@ -241,14 +411,17 @@ export type Fee = {
updatedAt: string;
};
export async function getFees(props: { clientId: string; teamId: string }) {
const authToken = await getAuthToken();
export async function getFees(params: {
clientId: string;
teamId: string;
authToken: string;
}) {
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "GET",
});
@@ -262,23 +435,23 @@ export async function getFees(props: { clientId: string; teamId: string }) {
return json.data as Fee;
}
export async function updateFee(props: {
export async function updateFee(params: {
clientId: string;
teamId: string;
feeRecipient: string;
feeBps: number;
authToken: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
body: JSON.stringify({
feeBps: props.feeBps,
feeRecipient: props.feeRecipient,
feeBps: params.feeBps,
feeRecipient: params.feeRecipient,
}),
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "PUT",
});
@@ -288,7 +461,7 @@ export async function updateFee(props: {
throw new Error(text);
}
return;
return true;
}
export type PaymentsResponse = {
@@ -345,36 +518,34 @@ export type Payment = {
export type BridgePayment = Extract<Payment, { type: BridgePaymentType }>;
export async function getPayments(props: {
export async function getPayments(params: {
clientId: string;
teamId: string;
paymentLinkId?: string;
limit?: number;
authToken: string;
offset?: number;
}) {
const authToken = await getAuthToken();
// Build URL with query parameters if provided
const url = new URL(`${UB_BASE_URL}/v1/developer/payments`);
if (props.limit) {
url.searchParams.append("limit", props.limit.toString());
if (params.limit) {
url.searchParams.append("limit", params.limit.toString());
}
if (props.offset) {
url.searchParams.append("offset", props.offset.toString());
if (params.offset) {
url.searchParams.append("offset", params.offset.toString());
}
if (props.paymentLinkId) {
url.searchParams.append("paymentLinkId", props.paymentLinkId);
if (params.paymentLinkId) {
url.searchParams.append("paymentLinkId", params.paymentLinkId);
}
const res = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${authToken}`,
Authorization: `Bearer ${params.authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
"x-client-id": params.clientId,
"x-team-id": params.teamId,
},
method: "GET",
});
@@ -1,192 +0,0 @@
"use client";
/* eslint-disable no-restricted-syntax */
import { Mesh, Program, Renderer, Triangle, Vec2 } from "ogl";
import { useEffect, useRef } from "react";
const vertex = `
attribute vec2 position;
void main(){gl_Position=vec4(position,0.0,1.0);}
`;
const fragment = `
#ifdef GL_ES
precision lowp float;
#endif
uniform vec2 uResolution;
uniform float uTime;
uniform float uHueShift;
uniform float uNoise;
uniform float uScan;
uniform float uScanFreq;
uniform float uWarp;
uniform vec3 uBackgroundColor;
uniform float uBackgroundOpacity;
uniform float uPatternLightness;
#define iTime uTime
#define iResolution uResolution
vec4 buf[8];
float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}
mat3 rgb2yiq=mat3(0.299,0.587,0.114,0.596,-0.274,-0.322,0.211,-0.523,0.312);
mat3 yiq2rgb=mat3(1.0,0.956,0.621,1.0,-0.272,-0.647,1.0,-1.106,1.703);
vec3 hueShiftRGB(vec3 col,float deg){
vec3 yiq=rgb2yiq*col;
float rad=radians(deg);
float cosh=cos(rad),sinh=sin(rad);
vec3 yiqShift=vec3(yiq.x,yiq.y*cosh-yiq.z*sinh,yiq.y*sinh+yiq.z*cosh);
return clamp(yiq2rgb*yiqShift,0.0,1.0);
}
vec4 sigmoid(vec4 x){return 1./(1.+exp(-x));}
vec4 cppn_fn(vec2 coordinate,float in0,float in1,float in2){
buf[6]=vec4(coordinate.x,coordinate.y,0.3948333106474662+in0,0.36+in1);
buf[7]=vec4(0.14+in2,sqrt(coordinate.x*coordinate.x+coordinate.y*coordinate.y),0.,0.);
buf[0]=mat4(vec4(6.5404263,-3.6126034,0.7590882,-1.13613),vec4(2.4582713,3.1660357,1.2219609,0.06276096),vec4(-5.478085,-6.159632,1.8701609,-4.7742867),vec4(6.039214,-5.542865,-0.90925294,3.251348))*buf[6]+mat4(vec4(0.8473259,-5.722911,3.975766,1.6522468),vec4(-0.24321538,0.5839259,-1.7661959,-5.350116),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(0.21808943,1.1243913,-1.7969975,5.0294676);
buf[1]=mat4(vec4(-3.3522482,-6.0612736,0.55641043,-4.4719114),vec4(0.8631464,1.7432913,5.643898,1.6106541),vec4(2.4941394,-3.5012043,1.7184316,6.357333),vec4(3.310376,8.209261,1.1355612,-1.165539))*buf[6]+mat4(vec4(5.24046,-13.034365,0.009859298,15.870829),vec4(2.987511,3.129433,-0.89023495,-1.6822904),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-5.9457836,-6.573602,-0.8812491,1.5436668);
buf[0]=sigmoid(buf[0]);buf[1]=sigmoid(buf[1]);
buf[2]=mat4(vec4(-15.219568,8.095543,-2.429353,-1.9381982),vec4(-5.951362,4.3115187,2.6393783,1.274315),vec4(-7.3145227,6.7297835,5.2473326,5.9411426),vec4(5.0796127,8.979051,-1.7278991,-1.158976))*buf[6]+mat4(vec4(-11.967154,-11.608155,6.1486754,11.237008),vec4(2.124141,-6.263192,-1.7050359,-0.7021966),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-4.17164,-3.2281182,-4.576417,-3.6401186);
buf[3]=mat4(vec4(3.1832156,-13.738922,1.879223,3.233465),vec4(0.64300746,12.768129,1.9141049,0.50990224),vec4(-0.049295485,4.4807224,1.4733979,1.801449),vec4(5.0039253,13.000481,3.3991797,-4.5561905))*buf[6]+mat4(vec4(-0.1285731,7.720628,-3.1425676,4.742367),vec4(0.6393625,3.714393,-0.8108378,-0.39174938),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-1.1811101,-21.621881,0.7851888,1.2329718);
buf[2]=sigmoid(buf[2]);buf[3]=sigmoid(buf[3]);
buf[4]=mat4(vec4(5.214916,-7.183024,2.7228765,2.6592617),vec4(-5.601878,-25.3591,4.067988,0.4602802),vec4(-10.57759,24.286327,21.102104,37.546658),vec4(4.3024497,-1.9625226,2.3458803,-1.372816))*buf[0]+mat4(vec4(-17.6526,-10.507558,2.2587414,12.462782),vec4(6.265566,-502.75443,-12.642513,0.9112289),vec4(-10.983244,20.741234,-9.701768,-0.7635988),vec4(5.383626,1.4819539,-4.1911616,-4.8444734))*buf[1]+mat4(vec4(12.785233,-16.345072,-0.39901125,1.7955981),vec4(-30.48365,-1.8345358,1.4542528,-1.1118771),vec4(19.872723,-7.337935,-42.941723,-98.52709),vec4(8.337645,-2.7312303,-2.2927687,-36.142323))*buf[2]+mat4(vec4(-16.298317,3.5471997,-0.44300047,-9.444417),vec4(57.5077,-35.609753,16.163465,-4.1534753),vec4(-0.07470326,-3.8656476,-7.0901804,3.1523974),vec4(-12.559385,-7.077619,1.490437,-0.8211543))*buf[3]+vec4(-7.67914,15.927437,1.3207729,-1.6686112);
buf[5]=mat4(vec4(-1.4109162,-0.372762,-3.770383,-21.367174),vec4(-6.2103205,-9.35908,0.92529047,8.82561),vec4(11.460242,-22.348068,13.625772,-18.693201),vec4(-0.3429052,-3.9905605,-2.4626114,-0.45033523))*buf[0]+mat4(vec4(7.3481627,-4.3661838,-6.3037653,-3.868115),vec4(1.5462853,6.5488915,1.9701879,-0.58291394),vec4(6.5858274,-2.2180402,3.7127688,-1.3730392),vec4(-5.7973905,10.134961,-2.3395722,-5.965605))*buf[1]+mat4(vec4(-2.5132585,-6.6685553,-1.4029363,-0.16285264),vec4(-0.37908727,0.53738135,4.389061,-1.3024765),vec4(-0.70647055,2.0111287,-5.1659346,-3.728635),vec4(-13.562562,10.487719,-0.9173751,-2.6487076))*buf[2]+mat4(vec4(-8.645013,6.5546675,-6.3944063,-5.5933375),vec4(-0.57783127,-1.077275,36.91025,5.736769),vec4(14.283112,3.7146652,7.1452246,-4.5958776),vec4(2.7192075,3.6021907,-4.366337,-2.3653464))*buf[3]+vec4(-5.9000807,-4.329569,1.2427121,8.59503);
buf[4]=sigmoid(buf[4]);buf[5]=sigmoid(buf[5]);
buf[6]=mat4(vec4(-1.61102,0.7970257,1.4675229,0.20917463),vec4(-28.793737,-7.1390953,1.5025433,4.656581),vec4(-10.94861,39.66238,0.74318546,-10.095605),vec4(-0.7229728,-1.5483948,0.7301322,2.1687684))*buf[0]+mat4(vec4(3.2547753,21.489103,-1.0194173,-3.3100595),vec4(-3.7316632,-3.3792162,-7.223193,-0.23685838),vec4(13.1804495,0.7916005,5.338587,5.687114),vec4(-4.167605,-17.798311,-6.815736,-1.6451967))*buf[1]+mat4(vec4(0.604885,-7.800309,-7.213122,-2.741014),vec4(-3.522382,-0.12359311,-0.5258442,0.43852118),vec4(9.6752825,-22.853785,2.062431,0.099892326),vec4(-4.3196306,-17.730087,2.5184598,5.30267))*buf[2]+mat4(vec4(-6.545563,-15.790176,-6.0438633,-5.415399),vec4(-43.591583,28.551912,-16.00161,18.84728),vec4(4.212382,8.394307,3.0958717,8.657522),vec4(-5.0237565,-4.450633,-4.4768,-5.5010443))*buf[3]+mat4(vec4(1.6985557,-67.05806,6.897715,1.9004834),vec4(1.8680354,2.3915145,2.5231109,4.081538),vec4(11.158006,1.7294737,2.0738268,7.386411),vec4(-4.256034,-306.24686,8.258898,-17.132736))*buf[4]+mat4(vec4(1.6889864,-4.5852966,3.8534803,-6.3482175),vec4(1.3543309,-1.2640043,9.932754,2.9079645),vec4(-5.2770967,0.07150358,-0.13962056,3.3269649),vec4(28.34703,-4.918278,6.1044083,4.085355))*buf[5]+vec4(6.6818056,12.522166,-3.7075126,-4.104386);
buf[7]=mat4(vec4(-8.265602,-4.7027016,5.098234,0.7509808),vec4(8.6507845,-17.15949,16.51939,-8.884479),vec4(-4.036479,-2.3946867,-2.6055532,-1.9866527),vec4(-2.2167742,-1.8135649,-5.9759874,4.8846445))*buf[0]+mat4(vec4(6.7790847,3.5076547,-2.8191125,-2.7028968),vec4(-5.743024,-0.27844876,1.4958696,-5.0517144),vec4(13.122226,15.735168,-2.9397483,-4.101023),vec4(-14.375265,-5.030483,-6.2599335,2.9848232))*buf[1]+mat4(vec4(4.0950394,-0.94011575,-5.674733,4.755022),vec4(4.3809423,4.8310084,1.7425908,-3.437416),vec4(2.117492,0.16342592,-104.56341,16.949184),vec4(-5.22543,-2.994248,3.8350096,-1.9364246))*buf[2]+mat4(vec4(-5.900337,1.7946124,-13.604192,-3.8060522),vec4(6.6583457,31.911177,25.164474,91.81147),vec4(11.840538,4.1503043,-0.7314397,6.768467),vec4(-6.3967767,4.034772,6.1714606,-0.32874924))*buf[3]+mat4(vec4(3.4992442,-196.91893,-8.923708,2.8142626),vec4(3.4806502,-3.1846354,5.1725626,5.1804223),vec4(-2.4009497,15.585794,1.2863957,2.0252278),vec4(-71.25271,-62.441242,-8.138444,0.50670296))*buf[4]+mat4(vec4(-12.291733,-11.176166,-7.3474145,4.390294),vec4(10.805477,5.6337385,-0.9385842,-4.7348723),vec4(-12.869276,-7.039391,5.3029537,7.5436664),vec4(1.4593618,8.91898,3.5101583,5.840625))*buf[5]+vec4(2.2415268,-6.705987,-0.98861027,-2.117676);
buf[6]=sigmoid(buf[6]);buf[7]=sigmoid(buf[7]);
buf[0]=mat4(vec4(1.6794263,1.3817469,2.9625452,0.),vec4(-1.8834411,-1.4806935,-3.5924516,0.),vec4(-1.3279216,-1.0918057,-2.3124623,0.),vec4(0.2662234,0.23235129,0.44178495,0.))*buf[0]+mat4(vec4(-0.6299101,-0.5945583,-0.9125601,0.),vec4(0.17828953,0.18300213,0.18182953,0.),vec4(-2.96544,-2.5819945,-4.9001055,0.),vec4(1.4195864,1.1868085,2.5176322,0.))*buf[1]+mat4(vec4(-1.2584374,-1.0552157,-2.1688404,0.),vec4(-0.7200217,-0.52666044,-1.438251,0.),vec4(0.15345335,0.15196142,0.272854,0.),vec4(0.945728,0.8861938,1.2766753,0.))*buf[2]+mat4(vec4(-2.4218085,-1.968602,-4.35166,0.),vec4(-22.683098,-18.0544,-41.954372,0.),vec4(0.63792,0.5470648,1.1078634,0.),vec4(-1.5489894,-1.3075932,-2.6444845,0.))*buf[3]+mat4(vec4(-0.49252132,-0.39877754,-0.91366625,0.),vec4(0.95609266,0.7923952,1.640221,0.),vec4(0.30616966,0.15693925,0.8639857,0.),vec4(1.1825981,0.94504964,2.176963,0.))*buf[4]+mat4(vec4(0.35446745,0.3293795,0.59547555,0.),vec4(-0.58784515,-0.48177817,-1.0614829,0.),vec4(2.5271258,1.9991658,4.6846647,0.),vec4(0.13042648,0.08864098,0.30187556,0.))*buf[5]+mat4(vec4(-1.7718065,-1.4033192,-3.3355875,0.),vec4(3.1664357,2.638297,5.378702,0.),vec4(-3.1724713,-2.6107926,-5.549295,0.),vec4(-2.851368,-2.249092,-5.3013067,0.))*buf[6]+mat4(vec4(1.5203838,1.2212278,2.8404984,0.),vec4(1.5210563,1.2651345,2.683903,0.),vec4(2.9789467,2.4364579,5.2347264,0.),vec4(2.2270417,1.8825914,3.8028636,0.))*buf[7]+vec4(-1.5468478,-3.6171484,0.24762098,0.);
buf[0]=sigmoid(buf[0]);
return vec4(buf[0].x,buf[0].y,buf[0].z,1.);
}
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 uv=fragCoord/uResolution.xy*2.-1.;
uv.y*=-1.;
uv+=uWarp*vec2(sin(uv.y*6.283+uTime*0.5),cos(uv.x*6.283+uTime*0.5))*0.05;
fragColor=cppn_fn(uv,0.1*sin(0.3*uTime),0.1*sin(0.69*uTime),0.1*sin(0.44*uTime));
}
void main(){
vec4 col;mainImage(col,gl_FragCoord.xy);
col.rgb=hueShiftRGB(col.rgb,uHueShift);
float scanline_val=sin(gl_FragCoord.y*uScanFreq)*0.5+0.5;
col.rgb*=1.-(scanline_val*scanline_val)*uScan;
col.rgb+=(rand(gl_FragCoord.xy+uTime)-0.5)*uNoise;
// Apply lightness control to pattern
col.rgb = col.rgb * uPatternLightness;
// Use background color as base, then add pattern on top
vec3 finalColor = uBackgroundColor + col.rgb * uBackgroundOpacity;
gl_FragColor=vec4(clamp(finalColor,0.0,1.0),1.0);
}
`;
type Props = {
hueShift?: number;
noiseIntensity?: number;
scanlineIntensity?: number;
speed?: number;
scanlineFrequency?: number;
warpAmount?: number;
resolutionScale?: number;
backgroundColor?: [number, number, number];
backgroundOpacity?: number;
patternLightness?: number;
};
export function DarkVeil({
hueShift = 0,
noiseIntensity = 0,
scanlineIntensity = 0,
speed = 0.5,
scanlineFrequency = 0,
warpAmount = 0,
resolutionScale = 1,
backgroundColor = [0, 0, 0],
backgroundOpacity = 1,
patternLightness = 1.0,
}: Props) {
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = ref.current as HTMLCanvasElement;
const parent = canvas.parentElement as HTMLElement;
const renderer = new Renderer({
dpr: Math.min(window.devicePixelRatio, 2),
canvas,
});
const gl = renderer.gl;
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex,
fragment,
uniforms: {
uTime: { value: 0 },
uResolution: { value: new Vec2() },
uHueShift: { value: hueShift },
uNoise: { value: noiseIntensity },
uScan: { value: scanlineIntensity },
uScanFreq: { value: scanlineFrequency },
uWarp: { value: warpAmount },
uBackgroundColor: { value: backgroundColor },
uBackgroundOpacity: { value: backgroundOpacity },
uPatternLightness: { value: patternLightness },
},
});
const mesh = new Mesh(gl, { geometry, program });
const resize = () => {
const w = parent.clientWidth,
h = parent.clientHeight;
renderer.setSize(w * resolutionScale, h * resolutionScale);
program.uniforms.uResolution.value.set(w, h);
};
window.addEventListener("resize", resize);
resize();
const start = performance.now();
let frame = 0;
const loop = () => {
program.uniforms.uTime.value =
((performance.now() - start) / 1000) * speed;
program.uniforms.uHueShift.value = hueShift;
program.uniforms.uNoise.value = noiseIntensity;
program.uniforms.uScan.value = scanlineIntensity;
program.uniforms.uScanFreq.value = scanlineFrequency;
program.uniforms.uWarp.value = warpAmount;
program.uniforms.uBackgroundColor.value = backgroundColor;
program.uniforms.uBackgroundOpacity.value = backgroundOpacity;
program.uniforms.uPatternLightness.value = patternLightness;
renderer.render({ scene: mesh });
frame = requestAnimationFrame(loop);
};
loop();
return () => {
cancelAnimationFrame(frame);
window.removeEventListener("resize", resize);
};
}, [
hueShift,
noiseIntensity,
scanlineIntensity,
speed,
scanlineFrequency,
warpAmount,
resolutionScale,
backgroundColor,
backgroundOpacity,
patternLightness,
]);
return <canvas ref={ref} className="w-full h-full block bg-background" />;
}
@@ -240,9 +240,10 @@ function TabButton(props: {
return (
<Button
onClick={props.onClick}
size="sm"
className={cn(
"rounded-full text-muted-foreground px-5 text-base bg-accent",
props.isActive && "text-foreground border-foreground",
"rounded-full text-muted-foreground px-4 bg-accent",
props.isActive && "text-foreground border-foreground/50",
)}
variant="outline"
>
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { getUnixTime, subDays } from "date-fns";
import { isProd } from "@/constants/env-utils";
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
@@ -35,6 +36,11 @@ export function useTokenTransfers(params: {
url.searchParams.set("page", params.page.toString());
url.searchParams.set("limit", params.limit.toString());
url.searchParams.set("clientId", NEXT_PUBLIC_DASHBOARD_CLIENT_ID);
const THIRTY_DAYS_AGO = subDays(new Date(), 30);
url.searchParams.set(
"block_timestamp_from",
getUnixTime(THIRTY_DAYS_AGO).toString(),
);
const res = await fetch(url);
if (!res.ok) {
@@ -46,7 +52,12 @@ export function useTokenTransfers(params: {
return data;
},
queryKey: ["token-transfers", params],
refetchInterval: 5000,
refetchInterval: (data) => {
if (data?.state.error) {
return false;
}
return 5000;
},
refetchOnWindowFocus: false,
retry: false,
retryOnMount: false,
@@ -0,0 +1,120 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import type { Status } from "thirdweb/bridge";
import { storybookThirdwebClient } from "@/storybook/utils";
import { BridgeStatus } from "./bridge-status";
const meta = {
component: BridgeStatus,
title: "chain/tx/bridge-status",
decorators: [
(Story) => (
<div className="container max-w-6xl py-10">
<Story />
</div>
),
],
} satisfies Meta<typeof BridgeStatus>;
export default meta;
type Story = StoryObj<typeof meta>;
const completedStatus: Status = {
status: "COMPLETED",
paymentId:
"0x8e0a0d9c0254a75a3004a28e0fb69bae7f2d659938ddd461753627270930db60",
originAmount: 129935n,
destinationAmount: 100000n,
transactions: [
{
chainId: 42161,
transactionHash:
"0x37daf698878e73590adb2d6f833e810304a9981829ad983fb355245af1183107",
},
{
chainId: 8453,
transactionHash:
"0xc72abba2761bb48f605125dc12c17b15088303db960816fa12316f0c90796417",
},
],
originChainId: 42161,
originTokenAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
originToken: {
chainId: 42161,
address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
symbol: "USDC",
name: "USD Coin",
decimals: 6,
iconUri: "https://ethereum-optimism.github.io/data/USDC/logo.png",
},
destinationChainId: 8453,
destinationTokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
destinationToken: {
chainId: 8453,
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
symbol: "USDC",
name: "USD Coin",
decimals: 6,
iconUri:
"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694",
},
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
};
const pendingStatus: Status = {
...completedStatus,
status: "PENDING",
};
const failedStatus: Status = {
...completedStatus,
status: "FAILED",
};
const notFoundStatus: Status = {
...completedStatus,
status: "NOT_FOUND",
transactions: [],
};
export const Completed: Story = {
args: {
bridgeStatus: completedStatus,
client: storybookThirdwebClient,
},
};
export const Pending: Story = {
args: {
bridgeStatus: pendingStatus,
client: storybookThirdwebClient,
},
};
export const Failed: Story = {
args: {
bridgeStatus: failedStatus,
client: storybookThirdwebClient,
},
};
export const NotFound: Story = {
args: {
bridgeStatus: notFoundStatus,
client: storybookThirdwebClient,
},
};
export const WithPurchaseData: Story = {
args: {
bridgeStatus: {
...completedStatus,
purchaseData: {
userId: "68d645b7ded999651272bf1e",
credits: 32000,
transactionId: "fd2606d1-90df-45c6-bd2c-b19a34764a31",
},
},
client: storybookThirdwebClient,
},
};
@@ -0,0 +1,404 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { CodeClient } from "@workspace/ui/components/code/code.client";
import { Img } from "@workspace/ui/components/img";
import { Spinner } from "@workspace/ui/components/spinner";
import { ArrowRightIcon, CircleCheckIcon, CircleXIcon } from "lucide-react";
import Link from "next/link";
import { NATIVE_TOKEN_ADDRESS, type ThirdwebClient } from "thirdweb";
import type { Status, Token } from "thirdweb/bridge";
import { status } from "thirdweb/bridge";
import { toTokens } from "thirdweb/utils";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { fetchChain } from "@/utils/fetchChain";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
type PurchaseData = Exclude<Status, { status: "NOT_FOUND" }>["purchaseData"];
export function BridgeStatus(props: {
bridgeStatus: Status;
client: ThirdwebClient;
}) {
const { bridgeStatus } = props;
const purchaseDataString =
bridgeStatus.status !== "NOT_FOUND" && bridgeStatus.purchaseData
? getPurchaseData(bridgeStatus.purchaseData)
: undefined;
return (
<div className="bg-card rounded-xl border relative">
{bridgeStatus.status === "FAILED" &&
bridgeStatus.transactions.length > 0 && (
<FailedBridgeStatusContent
transactions={bridgeStatus.transactions}
client={props.client}
/>
)}
{(bridgeStatus.status === "COMPLETED" ||
bridgeStatus.status === "PENDING") && (
<BridgeStatusContent
bridgeStatus={bridgeStatus}
client={props.client}
/>
)}
<div className="px-6 lg:px-8 py-7 space-y-1.5">
<div className="flex justify-between items-center">
<p className="text-sm text-muted-foreground "> Status </p>
<div
className={cn(
"flex items-center gap-1.5 text-sm capitalize",
bridgeStatus.status === "COMPLETED" && "text-green-500",
bridgeStatus.status === "PENDING" && "text-blue-500",
bridgeStatus.status === "FAILED" && "text-red-500",
)}
>
{bridgeStatus.status === "COMPLETED" && (
<CircleCheckIcon className="size-3.5" />
)}
{bridgeStatus.status === "PENDING" && (
<Spinner className="size-3.5" />
)}
{bridgeStatus.status === "FAILED" && (
<CircleXIcon className="size-3.5" />
)}
{bridgeStatus.status === "NOT_FOUND" && (
<CircleXIcon className="size-3.5" />
)}
{bridgeStatus.status.toLowerCase().replace("_", " ")}
</div>
</div>
<div className="flex justify-between items-center">
<p className="text-sm text-muted-foreground ">Payment ID</p>
<CopyTextButton
textToCopy={bridgeStatus.paymentId}
textToShow={`${bridgeStatus.paymentId.slice(0, 6)}...${bridgeStatus.paymentId.slice(-4)}`}
tooltip="Payment ID"
className="text-sm translate-x-1.5"
copyIconPosition="left"
variant="ghost"
/>
</div>
</div>
{purchaseDataString && (
<div className="px-6 lg:px-8 py-7 space-y-2 border-t border-dashed">
<p className="text-sm text-muted-foreground ">Purchase Data</p>
<CodeClient
code={purchaseDataString}
lang="json"
className="[&_code]:text-xs"
scrollableClassName="p-3"
/>
</div>
)}
</div>
);
}
function TokenInfo(props: {
label: string;
addressLabel: string;
token: Token;
amountWei: bigint | undefined;
walletAddress: string;
client: ThirdwebClient;
txHash: string | undefined;
}) {
const chainQuery = useChainQuery(props.token.chainId);
return (
<div className="flex-1 pt-10 pb-9 px-6 lg:px-8">
<div className="flex justify-between items-center">
<h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
{props.label}
</h3>
</div>
<div className="pb-7 pt-6 border-b border-dashed">
<div className="flex items-center gap-3">
<div className="relative hover:ring-2 hover:ring-offset-2 hover:ring-offset-card hover:ring-foreground/30 rounded-full">
<Link
href={
props.token.address.toLowerCase() ===
NATIVE_TOKEN_ADDRESS.toLowerCase()
? `/${chainQuery.data?.slug || props.token.chainId}`
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
}
target="_blank"
aria-label="View Token"
className="absolute inset-0 z-10"
/>
<Img
src={props.token.iconUri}
alt={props.token.name}
className="size-10 rounded-full"
fallback={
<div className="size-10 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
}
/>
<div className="absolute -bottom-0.5 -right-0.5 bg-background p-0.5 rounded-full">
<Img
src={resolveSchemeWithErrorHandler({
client: props.client,
uri: chainQuery.data?.icon?.url,
})}
alt={props.token.name}
className="size-4 rounded-full"
fallback={
<div className="size-4 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
}
/>
</div>
</div>
<div className="space-y-1.5">
<p className="text-lg text-foreground font-medium tracking-tight leading-none">
{props.amountWei ? (
<>
{toTokens(props.amountWei, props.token.decimals)}{" "}
{props.token.symbol}
</>
) : (
"N/A"
)}
</p>
<SkeletonContainer
skeletonData={`Chain ${props.token.chainId}`}
loadedData={chainQuery.data?.name}
className="[&_*]:flex"
render={(data) => (
<Link
href={`/${chainQuery.data?.slug || props.token.chainId}`}
className="text-sm text-muted-foreground leading-none hover:underline"
target="_blank"
>
{data}
</Link>
)}
/>
</div>
</div>
</div>
<div className="h-6" />
<div className="space-y-1.5">
<div className="flex gap-2.5 justify-between">
<p className="text-sm text-muted-foreground ">{props.addressLabel}</p>
<WalletAddress
address={props.walletAddress}
client={props.client}
className="py-0.5 h-auto text-sm [&>*]:!font-sans"
iconClassName="size-3.5"
/>
</div>
<div className="flex gap-2 justify-between">
<p className="text-sm text-muted-foreground ">Token Address</p>
<CopyTextButton
textToCopy={props.token.address}
textToShow={`${props.token.address.slice(0, 6)}...${props.token.address.slice(-4)}`}
tooltip="Token Address"
className="text-sm translate-x-1.5"
copyIconPosition="left"
variant="ghost"
/>
</div>
<div className="flex gap-2 justify-between">
<p className="text-sm text-muted-foreground ">Transaction Hash</p>
{props.txHash ? (
<CopyTextButton
textToCopy={props.txHash}
textToShow={`${props.txHash.slice(0, 6)}...${props.txHash.slice(-4)}`}
tooltip="Transaction Hash"
className="text-sm translate-x-1.5"
copyIconPosition="left"
variant="ghost"
/>
) : (
<p className="text-sm text-muted-foreground ">N/A</p>
)}
</div>
</div>
</div>
);
}
function BridgeStatusContent(props: {
bridgeStatus: Exclude<Status, { status: "FAILED" } | { status: "NOT_FOUND" }>;
client: ThirdwebClient;
}) {
const { bridgeStatus } = props;
const fromTxHash = bridgeStatus.transactions.find(
(tx) => tx.chainId === bridgeStatus.originChainId,
)?.transactionHash;
const toTxHash = bridgeStatus.transactions.find(
(tx) => tx.chainId === bridgeStatus.destinationChainId,
)?.transactionHash;
return (
<div className="grid grid-cols-1 lg:grid-cols-2 border-b border-dashed">
<TokenInfo
label="From"
token={bridgeStatus.originToken}
amountWei={bridgeStatus.originAmount}
walletAddress={bridgeStatus.sender}
client={props.client}
txHash={fromTxHash}
addressLabel="Sender"
/>
<div className="border-t lg:border-l lg:border-t-0 border-dashed relative">
<div className="p-1.5 rounded-full bg-background absolute top-0 lg:top-1/2 left-1/2 lg:left-0 -translate-y-1/2 -translate-x-1/2 border">
<ArrowRightIcon className="size-4 text-muted-foreground rotate-90 lg:rotate-0" />
</div>
<TokenInfo
label="To"
token={bridgeStatus.destinationToken}
// TODO - destinationAmount is not available in PENDING status, fix it in API
amountWei={
bridgeStatus.status === "COMPLETED"
? bridgeStatus.destinationAmount
: undefined
}
walletAddress={bridgeStatus.receiver}
client={props.client}
txHash={toTxHash}
addressLabel="Receiver"
/>
</div>
</div>
);
}
function FailedBridgeStatusContent(props: {
transactions: { chainId: number; transactionHash: string }[];
client: ThirdwebClient;
}) {
return (
<div className="px-6 lg:px-8 py-7 space-y-1.5 border-b border-dashed">
<h3 className="text-base font-medium tracking-tight mb-3">
Transactions
</h3>
<div className="flex flex-col gap-2">
{props.transactions.map((tx) => {
return (
<TxHashRow
key={tx.chainId}
client={props.client}
chainId={tx.chainId}
txHash={tx.transactionHash}
/>
);
})}
</div>
</div>
);
}
function useChainQuery(chainId: number | undefined) {
return useQuery({
enabled: !!chainId,
queryKey: ["chain-metadata", chainId],
queryFn: async () => {
if (!chainId) {
return null;
}
return fetchChain(chainId);
},
});
}
function TxHashRow(props: {
client: ThirdwebClient;
chainId: number;
txHash: string;
}) {
const chainQuery = useChainQuery(props.chainId);
return (
<div className="flex items-center gap-2 justify-between">
<div className="flex items-center gap-2">
<Img
src={resolveSchemeWithErrorHandler({
client: props.client,
uri: chainQuery.data?.icon?.url,
})}
alt={chainQuery.data?.name}
className="size-4 rounded-full"
fallback={
<div className="size-4 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
}
/>
<SkeletonContainer
skeletonData={`Chain ${props.chainId}`}
loadedData={chainQuery.data?.name}
render={(data) => (
<p className="text-sm text-muted-foreground">{data}</p>
)}
/>
</div>
<CopyTextButton
textToCopy={props.txHash}
textToShow={`${props.txHash.slice(0, 6)}...${props.txHash.slice(-4)}`}
tooltip="Copy Transaction Hash"
className="text-sm"
copyIconPosition="right"
variant="ghost"
/>
</div>
);
}
export function BridgeStatusWithPolling(props: {
bridgeStatus: Status;
client: ThirdwebClient;
chainId: number;
transactionHash: string;
}) {
const bridgeStatusQuery = useQuery({
enabled:
props.bridgeStatus.status === "PENDING" ||
props.bridgeStatus.status === "NOT_FOUND",
queryKey: ["bridge-status", props.transactionHash, props.chainId],
queryFn: () =>
status({
chainId: props.chainId,
transactionHash: props.transactionHash as `0x${string}`,
client: props.client,
}),
refetchInterval(query) {
const status = query.state.data?.status;
if (status === "COMPLETED" || status === "FAILED") {
return false;
}
return 5000;
},
});
return (
<BridgeStatus
bridgeStatus={bridgeStatusQuery.data || props.bridgeStatus}
client={props.client}
/>
);
}
function getPurchaseData(purchaseData: PurchaseData) {
try {
return JSON.stringify(purchaseData, null, 2);
} catch {
return undefined;
}
}
@@ -1,16 +1,42 @@
import { toTokens, ZERO_ADDRESS } from "thirdweb";
import { Button } from "@workspace/ui/components/button";
import { PlainTextCodeBlock } from "@workspace/ui/components/code/plaintext-code";
import { ToolTipLabel } from "@workspace/ui/components/tooltip";
import { formatDate, formatDistanceToNowStrict, fromUnixTime } from "date-fns";
import {
CircleAlertIcon,
CircleCheckIcon,
Clock4Icon,
InfoIcon,
} from "lucide-react";
import { toTokens } from "thirdweb";
import { status } from "thirdweb/bridge";
import type { ChainMetadata } from "thirdweb/chains";
import {
eth_getBlockByHash,
eth_getTransactionByHash,
eth_getTransactionReceipt,
getRpcClient,
} from "thirdweb/rpc";
import { hexToNumber, shortenAddress, toEther } from "thirdweb/utils";
import type { TransactionReceipt } from "thirdweb/transaction";
import { hexToNumber, toEther } from "thirdweb/utils";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
import { getChain } from "../../../utils";
import { BridgeStatusWithPolling } from "./bridge-status";
import { BridgeAndOverviewTabs } from "./tabs";
type Transaction = Awaited<ReturnType<typeof eth_getTransactionByHash>>;
export default async function Page(props: {
params: Promise<{ chain_id: string; txHash: `0x${string}` }>;
@@ -25,142 +51,339 @@ export default async function Page(props: {
client: serverThirdwebClient,
});
const [transaction, receipt] = await Promise.all([
const [transaction, receipt, bridgeStatus] = await Promise.all([
eth_getTransactionByHash(rpcRequest, {
hash: params.txHash,
}),
}).catch(() => undefined),
eth_getTransactionReceipt(rpcRequest, {
hash: params.txHash,
}),
}).catch(() => undefined),
status({
chainId: chain.chainId,
transactionHash: params.txHash,
client: serverThirdwebClient,
}).catch(() => undefined),
]);
if (!transaction.blockHash) {
return <div>no tx found</div>;
if (!transaction?.blockHash || !receipt) {
return (
<div className="flex flex-col items-center justify-center grow">
<div className="flex flex-col items-center justify-center gap-3">
<div className="p-2 rounded-full bg-background border">
<CircleAlertIcon className="size-5 text-muted-foreground" />
</div>
Transaction not found
</div>
</div>
);
}
const block = await eth_getBlockByHash(rpcRequest, {
blockHash: transaction.blockHash,
});
const overviewContent = (
<GeneericTxDetails
transaction={transaction}
receipt={receipt}
block={block}
chain={chain}
/>
);
return (
<main className="container flex flex-col gap-y-10 py-6">
<header className="flex flex-col gap-y-2">
<p className="font-bold text-3xl">Transaction Details</p>
<div className="flex gap-x-2">
{shortenAddress(transaction.from)} called on{" "}
{shortenAddress(transaction.to || ZERO_ADDRESS)}
</div>
</header>
<main className="pb-20">
<div className="flex h-14 items-center border-border border-b">
<Breadcrumb className="container max-w-6xl">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/chainlist">Chainlist</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href={`/${chain.slug}`}>
{chain.name.replace("Mainnet", "")}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Transaction Details</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<Separator orientation="horizontal" />
<div className="container max-w-6xl py-10">
<h1 className="font-semibold text-3xl tracking-tight">
Transaction Details
</h1>
</div>
<section className="flex flex-col gap-y-2">
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Transaction hash</div>
<p>{transaction.hash}</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">
Status {/* and method */}
</div>
<div className="flex gap-x-2">
<Badge
className={
receipt.status === "success" ? "bg-green-500" : "bg-red-500"
}
>
{receipt.status}
</Badge>
</div>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Block</div>
<p>{Number(transaction.blockNumber)}</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Timestamp</div>
<p>
{new Date(
Number.parseInt(block.timestamp.toString()) * 1000,
).toLocaleString()}
</p>
</div>
</section>
<Separator orientation="horizontal" />
<section className="flex flex-col gap-y-2">
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">From</div>
<p>{transaction.from}</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">
Interacted with contract
</div>
<p>{transaction.to}</p>
</div>
</section>
<Separator orientation="horizontal" />
<section className="flex flex-col gap-y-2">
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Value</div>
<p>{toEther(transaction.value)} ETH</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Transaction fee</div>
<p>{toEther((transaction.gasPrice || 0n) * receipt.gasUsed)} ETH</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Gas price</div>
<p>{toEther(transaction.gasPrice || 0n)} ETH</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Gas usage</div>
<p>{receipt.gasUsed.toString()}</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Gas fees (Gwei)</div>
<p>
Base: {toTokens(block.baseFeePerGas || 0n, 9)} | Max:{" "}
{toTokens(transaction.maxFeePerGas || 0n, 9)} | Max priority:{" "}
{transaction.maxPriorityFeePerGas?.toString()}
</p>
</div>
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Burnt fees</div>
<p>{toEther(block.baseFeePerGas || 0n * receipt.gasUsed)}</p>
</div>
</section>
<Separator orientation="horizontal" />
<section className="flex flex-col gap-y-2">
<div className="grid grid-cols-2">
<div className="flex items-center gap-x-2">Other</div>
<p>
Txn type: {hexToNumber(transaction.typeHex || "0x0")} (
{transaction.type}) | Nonce: {transaction.nonce} | Position:{" "}
{transaction.transactionIndex}
</p>
</div>
<div className="grid grid-cols-2 items-start">
<div className="flex items-center gap-x-2">Raw Input:</div>
<div className="break-words rounded-lg bg-card p-4 text-sm">
{transaction.input}
</div>
</div>
</section>
<div className="container max-w-6xl">
{bridgeStatus ? (
<BridgeAndOverviewTabs
bridgeStatus={
<BridgeStatusWithPolling
bridgeStatus={bridgeStatus}
client={getClientThirdwebClient()}
chainId={chain.chainId}
transactionHash={params.txHash}
/>
}
overview={overviewContent}
/>
) : (
overviewContent
)}
</div>
</main>
);
}
function GeneericTxDetails(props: {
transaction: Transaction;
receipt: TransactionReceipt;
block: {
timestamp: bigint;
baseFeePerGas: bigint | null;
};
chain: ChainMetadata;
}) {
const { transaction, receipt, chain, block } = props;
const timestamp = getDatefromTimestamp(block.timestamp);
return (
<div className="border rounded-xl p-4 lg:p-6 bg-card">
{/* section 1 */}
<section className="border-b pb-6 space-y-5 lg:space-y-3">
<GridItem
label="Transaction hash"
tooltip="A TxHash or transaction hash is a unique 66-character identifier that is generated whenever a transaction is executed."
>
<CopyTextButton
textToCopy={transaction.hash}
textToShow={transaction.hash}
tooltip="Copy transaction hash"
variant="ghost"
copyIconPosition="right"
className="truncate -translate-x-1.5 max-w-full"
/>
</GridItem>
<GridItem label="Status" tooltip="The status of the transaction.">
<Badge
className="py-0.5 gap-1.5 px-1.5 capitalize"
variant={receipt.status === "success" ? "success" : "destructive"}
>
{receipt.status === "success" ? (
<CircleCheckIcon className="size-3" />
) : (
<CircleAlertIcon className="size-3" />
)}
{receipt.status}
</Badge>
</GridItem>
{transaction.blockNumber && (
<GridItem
label="Block"
tooltip="Number of the block in which the transaction is recorded. Block confirmations indicate how many blocks have been added since the transaction was produced."
>
<p>{Number(transaction.blockNumber)}</p>
</GridItem>
)}
{timestamp && (
<GridItem
label="Timestamp"
tooltip="The date and time at which a transaction is validated."
>
<div className="flex items-center gap-1.5 flex-wrap">
<Clock4Icon className="size-3" />
<p>
{formatDistanceToNowStrict(timestamp, {
addSuffix: true,
})}
</p>
<p>({formatDate(timestamp, "PP pp z")})</p>
</div>
</GridItem>
)}
</section>
{/* section 2 */}
<section className="border-b py-6 space-y-5 lg:space-y-3">
<GridItem label="From" tooltip="The sending party of the transaction.">
<CopyTextButton
textToCopy={transaction.from}
textToShow={transaction.from}
tooltip="Copy from address"
variant="ghost"
copyIconPosition="right"
className="truncate -translate-x-1.5 max-w-full"
/>
</GridItem>
{transaction.to && (
<GridItem
label="Interacted with (To)"
tooltip="The receiving party of the transaction (could be a contract address)."
>
<CopyTextButton
textToCopy={transaction.to}
textToShow={transaction.to}
tooltip="Copy to address"
variant="ghost"
copyIconPosition="right"
className="truncate -translate-x-1.5 max-w-full"
/>
</GridItem>
)}
</section>
{/* section 3 */}
<section className="py-6 space-y-5 lg:space-y-3 border-b">
<GridItem
label="Value"
tooltip="The value being transacted in ETH and fiat value. Note: You can click the fiat value (if available) to see historical value at the time of transaction."
>
<p>
{toEther(transaction.value)} {chain.nativeCurrency?.symbol}
</p>
</GridItem>
<GridItem
label="Transaction Fee"
tooltip="Amount paid to the validator for processing the transaction."
>
<p>
{toEther((transaction.gasPrice || 0n) * receipt.gasUsed)}{" "}
{chain.nativeCurrency?.symbol}
</p>
</GridItem>
<GridItem
label="Gas Price"
tooltip="The price per gas unit the sender is willing to pay"
>
<p>
{toEther(transaction.gasPrice || 0n)} {chain.nativeCurrency?.symbol}
</p>
</GridItem>
<GridItem
label="Gas Usage"
tooltip="The amount of gas used by the transaction."
>
<p>{receipt.gasUsed.toString()}</p>
</GridItem>
<GridItem
label="Gas Fees"
tooltip="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively."
>
<p className="flex gap-2.5">
<span>Base: {toTokens(block.baseFeePerGas || 0n, 9)} Gwei</span>
{transaction.maxFeePerGas && (
<>
<span className="text-muted-foreground/50">|</span>
<span>
Max: {toTokens(transaction.maxFeePerGas || 0n, 9)} Gwei
</span>
</>
)}
{transaction.maxPriorityFeePerGas && (
<>
<span className="text-muted-foreground/50">|</span>
<span>
Max priority: {transaction.maxPriorityFeePerGas?.toString()}{" "}
Gwei
</span>
</>
)}
</p>
</GridItem>
<GridItem
label="Burnt fees"
tooltip="The amount of fees that were burnt by the transaction."
>
<p>{toEther(block.baseFeePerGas || 0n * receipt.gasUsed)}</p>
</GridItem>
</section>
{/* section 4 */}
<section className="pt-6 space-y-5 lg:space-y-3">
<GridItem
label="Other Attributes"
tooltip="Other data related to this transaction."
>
<div className="flex gap-2 flex-wrap">
<Badge variant="outline" className="gap-1 py-1">
<span className="text-muted-foreground">Txn type:</span>
<span className="capitalize">
{hexToNumber(transaction.typeHex || "0x0")} ({transaction.type})
</span>
</Badge>
<Badge variant="outline" className="gap-1 py-1">
<span className="text-muted-foreground">Nonce: </span>
<span>{transaction.nonce}</span>
</Badge>
<Badge variant="outline" className="gap-1 py-1">
<span className="text-muted-foreground">Position: </span>
<span>{transaction.transactionIndex}</span>
</Badge>
</div>
</GridItem>
<GridItem
label="Input Data"
tooltip="Additional data included for this transaction. Commonly used as part of contract interaction or as a message sent to the recipient."
>
<PlainTextCodeBlock
code={transaction.input}
className="[&_code]:!whitespace-pre-wrap break-all [&_code]:text-xs text-muted-foreground w-full"
scrollableClassName="max-h-[400px] p-2 min-h-[80px]"
/>
</GridItem>
</section>
</div>
);
}
function GridItem(props: {
label: string;
children: React.ReactNode;
tooltip?: string;
}) {
return (
<div className="flex flex-col gap-1 lg:grid grid-cols-[300px_1fr] items-start">
<div className="flex items-center gap-0.5">
<ToolTipLabel label={props.tooltip}>
<Button
asChild
variant="ghost"
className="h-auto w-auto p-1 hidden lg:block"
>
<div>
<InfoIcon className="size-3.5 text-muted-foreground" />
</div>
</Button>
</ToolTipLabel>
<p className="text-muted-foreground text-sm">{props.label}</p>
</div>
<div className="text-sm w-full">{props.children}</div>
</div>
);
}
function getDatefromTimestamp(timestamp: bigint) {
try {
return fromUnixTime(Number(timestamp));
} catch {
return undefined;
}
}
@@ -0,0 +1,31 @@
"use client";
import { useState } from "react";
import { TabButtons } from "@/components/ui/tabs";
export function BridgeAndOverviewTabs(props: {
bridgeStatus: React.ReactNode;
overview: React.ReactNode;
}) {
const [tab, setTab] = useState<"bridge" | "overview">("bridge");
return (
<div>
<TabButtons
tabs={[
{
name: "Bridge",
onClick: () => setTab("bridge"),
isActive: tab === "bridge",
},
{
name: "Overview",
onClick: () => setTab("overview"),
isActive: tab === "overview",
},
]}
/>
<div className="h-3" />
{tab === "bridge" && props.bridgeStatus}
{tab === "overview" && props.overview}
</div>
);
}
@@ -2,6 +2,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
import { Button } from "@workspace/ui/components/button";
import { Spinner } from "@workspace/ui/components/spinner";
import { PlusIcon } from "lucide-react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
@@ -19,7 +22,6 @@ import {
type RouteDiscoveryValidationSchema,
routeDiscoveryValidationSchema,
} from "@/schema/validations";
import { RouteDiscoveryCard } from "./RouteDiscoveryCard";
export const RouteDiscovery = ({
project,
@@ -77,33 +79,21 @@ export const RouteDiscovery = ({
},
);
const errorText = form.getFieldState("tokenAddress").error?.message;
return (
<Form {...form}>
<form autoComplete="off" onSubmit={handleSubmit}>
<RouteDiscoveryCard
bottomText=""
errorText={form.getFieldState("tokenAddress").error?.message}
noPermissionText={undefined}
saveButton={
// Only show the submit button in the default state
{
disabled: !form.formState.isDirty,
isPending: submitDiscoveryMutation.isPending,
type: "submit",
variant: "outline",
}
}
>
<div>
<h3 className="font-semibold text-xl tracking-tight">
<div className="relative rounded-lg border border-border bg-card">
<div className="relative border-dashed border-b px-4 py-6 lg:px-6">
<h3 className="font-semibold text-xl tracking-tight mb-1">
Add a token to Bridge
</h3>
<p className="mt-1.5 mb-4 text-muted-foreground max-w-3xl text-sm text-pretty">
<p className="mb-4 text-muted-foreground max-w-3xl text-sm text-pretty">
Select your chain and input the token address to automatically
kick-off the token route discovery process. <br /> This may take
up to 20-40 minutes to complete.
</p>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 max-w-3xl">
<FormField
control={form.control}
@@ -142,7 +132,33 @@ export const RouteDiscovery = ({
/>
</div>
</div>
</RouteDiscoveryCard>
<div>
<div className="flex items-center justify-between gap-2 px-4 py-4 lg:px-6">
{errorText ? (
<p className="text-destructive-text text-sm">{errorText}</p>
) : (
<div />
)}
<Button
className="gap-1.5 rounded-full"
disabled={
!form.formState.isDirty || submitDiscoveryMutation.isPending
}
size="sm"
type="submit"
variant={"outline"}
>
{submitDiscoveryMutation.isPending ? (
<Spinner className="size-3" />
) : (
<PlusIcon className="size-4 text-muted-foreground" />
)}
Add Token
</Button>
</div>
</div>
</div>
</form>
</Form>
);
@@ -1,84 +0,0 @@
import type React from "react";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/Spinner";
import { cn } from "@/lib/utils";
export function RouteDiscoveryCard(
props: React.PropsWithChildren<{
bottomText: React.ReactNode;
header?: {
description: string | undefined;
title: string;
};
errorText: string | undefined;
noPermissionText: string | undefined;
saveButton?: {
onClick?: () => void;
disabled: boolean;
isPending: boolean;
type?: "submit";
variant?:
| "ghost"
| "default"
| "primary"
| "destructive"
| "outline"
| "secondary";
className?: string;
label?: string;
};
}>,
) {
return (
<div className="relative rounded-lg border border-border bg-card">
<div
className={cn(
"relative border-border border-b px-4 py-6 lg:px-6",
props.noPermissionText && "cursor-not-allowed",
)}
>
{props.header && (
<>
<h3 className="font-semibold text-xl tracking-tight">
{props.header.title}
</h3>
{props.header.description && (
<p className="mt-1.5 mb-4 text-foreground text-sm">
{props.header.description}
</p>
)}
</>
)}
{props.children}
</div>
<div className="flex min-h-[60px] items-center justify-between gap-2 px-4 py-3 lg:px-6">
{props.noPermissionText ? (
<p className="text-muted-foreground text-sm">
{props.noPermissionText}
</p>
) : props.errorText ? (
<p className="text-destructive-text text-sm">{props.errorText}</p>
) : (
<p className="text-muted-foreground text-sm">{props.bottomText}</p>
)}
{props.saveButton && !props.noPermissionText && (
<Button
className={cn("gap-2", props.saveButton.className)}
disabled={props.saveButton.disabled || props.saveButton.isPending}
onClick={props.saveButton.onClick}
size="sm"
type={props.saveButton.type}
variant={props.saveButton.variant || "outline"}
>
{props.saveButton.isPending && <Spinner className="size-3" />}
{props.saveButton.label ||
(props.saveButton.isPending ? "Submit" : "Submit Token")}
</Button>
)}
</div>
</div>
);
}
@@ -11,6 +11,7 @@ import { PayAnalytics } from "../payments/components/PayAnalytics";
import { getUniversalBridgeFiltersFromSearchParams } from "../payments/components/time";
import { QuickStartSection } from "./QuickstartSection.client";
import { RouteDiscovery } from "./RouteDiscovery";
import { ViewTxStatus } from "./view-tx-status";
export default async function Page(props: {
params: Promise<{
@@ -84,7 +85,7 @@ export default async function Page(props: {
],
}}
>
<div className="flex flex-col gap-12">
<div className="flex flex-col gap-6">
<ResponsiveSearchParamsProvider value={searchParams}>
<PayAnalytics
client={client}
@@ -99,12 +100,16 @@ export default async function Page(props: {
<RouteDiscovery client={client} project={project} />
<QuickStartSection
projectSlug={params.project_slug}
teamSlug={params.team_slug}
clientId={project.publishableKey}
teamId={project.teamId}
/>
<ViewTxStatus client={client} />
<div className="pt-4">
<QuickStartSection
projectSlug={params.project_slug}
teamSlug={params.team_slug}
clientId={project.publishableKey}
teamId={project.teamId}
/>
</div>
</div>
</ProjectPage>
);
@@ -0,0 +1,72 @@
"use client";
import { Button } from "@workspace/ui/components/button";
import { ArrowUpRightIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function ViewTxStatus(props: { client: ThirdwebClient }) {
const [txHash, setTxHash] = useState("");
const [chainId, setChainId] = useState<number>(1);
const isValidTxHash = txHash.length === 66 && txHash.startsWith("0x");
const isEnabled = !!txHash && !!chainId && isValidTxHash;
return (
<div className="bg-card rounded-xl border">
<div className="p-4 lg:p-6 mb-3">
<h3 className="font-semibold text-xl tracking-tight mb-1">
View transaction status
</h3>
<p className="text-sm text-muted-foreground mb-4">
Select chain and enter the transaction hash to view the transaction
status.
</p>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 max-w-3xl">
<div className="space-y-2">
<Label>Chain</Label>
<SingleNetworkSelector
chainId={chainId}
onChange={setChainId}
client={props.client}
disableChainId
className="bg-background"
/>
</div>
<div className="space-y-2">
<Label htmlFor="view-tx-hash">Transaction Hash</Label>
<Input
id="view-tx-hash"
value={txHash}
onChange={(e) => setTxHash(e.target.value)}
placeholder="0x.."
/>
{txHash && !isValidTxHash && (
<p className="text-sm text-destructive-text">
Invalid transaction hash
</p>
)}
</div>
</div>
</div>
<div className="px-4 lg:px-6 py-4 border-t border-dashed justify-end items-center flex">
<Button
asChild
disabled={!isEnabled}
size="sm"
variant={isEnabled ? "default" : "outline"}
className="gap-1.5 rounded-full"
>
<Link href={`/${chainId.toString()}/tx/${txHash}`} target="_blank">
View Transaction Status <ArrowUpRightIcon className="size-4" />
</Link>
</Button>
</div>
</div>
);
}
@@ -156,6 +156,7 @@ export async function PayAnalytics(props: {
client={props.client}
projectClientId={props.projectClientId}
teamId={props.teamId}
authToken={props.authToken}
/>
</div>
);
@@ -28,6 +28,7 @@ export function PaymentHistory(props: {
client: ThirdwebClient;
projectClientId: string;
teamId: string;
authToken: string;
}) {
const [page, setPage] = useState(1);
const { data: payPurchaseData, isLoading } = useQuery<
@@ -40,6 +41,7 @@ export function PaymentHistory(props: {
limit: pageSize,
offset: (page - 1) * pageSize,
teamId: props.teamId,
authToken: props.authToken,
});
return res;
},
@@ -16,6 +16,7 @@ export function QuickStartSection(props: {
clientId: string;
teamId: string;
projectWalletAddress?: string;
authToken: string;
}) {
return (
<section>
@@ -46,6 +47,7 @@ export function QuickStartSection(props: {
clientId={props.clientId}
projectWalletAddress={props.projectWalletAddress}
teamId={props.teamId}
authToken={props.authToken}
>
<Button
className="w-full gap-2 group text-foreground bg-background"
@@ -49,6 +49,7 @@ export function CreatePaymentLinkButton(
clientId: string;
teamId: string;
projectWalletAddress?: string;
authToken: string;
}>,
) {
const [open, setOpen] = useState(false);
@@ -62,6 +63,7 @@ export function CreatePaymentLinkButton(
projectWalletAddress={props.projectWalletAddress}
setOpen={setOpen}
teamId={props.teamId}
authToken={props.authToken}
/>
</DialogContent>
</Dialog>
@@ -73,6 +75,7 @@ function CreatePaymentLinkDialogContent(props: {
teamId: string;
setOpen: (open: boolean) => void;
projectWalletAddress?: string;
authToken: string;
}) {
const client = getClientThirdwebClient();
@@ -125,6 +128,7 @@ function CreatePaymentLinkDialogContent(props: {
amount: toUnits(values.amount.toString(), token.decimals),
},
title: values.title,
authToken: props.authToken,
});
return result;
@@ -44,6 +44,7 @@ export function PaymentLinksTable(props: {
clientId: string;
teamId: string;
projectWalletAddress?: string;
authToken: string;
}) {
return (
<section>
@@ -57,6 +58,7 @@ export function PaymentLinksTable(props: {
clientId={props.clientId}
projectWalletAddress={props.projectWalletAddress}
teamId={props.teamId}
authToken={props.authToken}
/>
</section>
);
@@ -66,12 +68,14 @@ function PaymentLinksTableInner(props: {
clientId: string;
teamId: string;
projectWalletAddress?: string;
authToken: string;
}) {
const paymentLinksQuery = useQuery({
queryFn: async () => {
return getPaymentLinks({
clientId: props.clientId,
teamId: props.teamId,
authToken: props.authToken,
});
},
queryKey: ["payment-links", props.clientId, props.teamId],
@@ -83,6 +87,7 @@ function PaymentLinksTableInner(props: {
return await Promise.all(
paymentLinks.map(async (paymentLink) => {
const { data } = await getPayments({
authToken: props.authToken,
clientId: props.clientId,
teamId: props.teamId,
paymentLinkId: paymentLink.id,
@@ -134,6 +139,7 @@ function PaymentLinksTableInner(props: {
clientId={props.clientId}
projectWalletAddress={props.projectWalletAddress}
teamId={props.teamId}
authToken={props.authToken}
>
<Button className="gap-2 rounded-full" variant="default" size="sm">
<PlusIcon className="size-4" />
@@ -228,6 +234,7 @@ function PaymentLinksTableInner(props: {
clientId={props.clientId}
teamId={props.teamId}
paymentLinkId={paymentLink.id}
authToken={props.authToken}
>
<Button size="icon" variant="ghost">
<TrashIcon className="size-4" strokeWidth={1} />
@@ -277,6 +284,7 @@ function DeletePaymentLinkButton(
paymentLinkId: string;
clientId: string;
teamId: string;
authToken: string;
}>,
) {
const [open, setOpen] = useState(false);
@@ -287,6 +295,7 @@ function DeletePaymentLinkButton(
clientId: props.clientId,
teamId: props.teamId,
paymentLinkId: id,
authToken: props.authToken,
});
return null;
},
@@ -56,6 +56,7 @@ export default async function Page(props: {
clientId={project.publishableKey}
projectWalletAddress={projectWallet?.address}
teamId={project.teamId}
authToken={authToken}
>
<Button className="gap-1.5 rounded-full" size="sm">
<PlusIcon className="size-4" />
@@ -147,6 +148,7 @@ export default async function Page(props: {
clientId={project.publishableKey}
projectWalletAddress={projectWallet?.address}
teamId={project.teamId}
authToken={authToken}
/>
<QuickStartSection
projectSlug={params.project_slug}
@@ -154,6 +156,7 @@ export default async function Page(props: {
clientId={project.publishableKey}
projectWalletAddress={projectWallet?.address}
teamId={project.teamId}
authToken={authToken}
/>
<AdvancedSection
@@ -3,16 +3,26 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { formatDistanceToNow } from "date-fns";
import { PlusIcon, TrashIcon } from "lucide-react";
import {
DotIcon,
MoreVerticalIcon,
PencilIcon,
PlusIcon,
TrashIcon,
WebhookIcon,
} from "lucide-react";
import Link from "next/link";
import { type PropsWithChildren, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { randomPrivateKey } from "thirdweb/wallets";
import { z } from "zod";
import type { Webhook } from "@/api/universal-bridge/developer";
import {
createWebhook,
deleteWebhook,
getWebhooks,
updateWebhook,
} from "@/api/universal-bridge/developer";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { Button } from "@/components/ui/button";
@@ -27,6 +37,12 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Form,
FormDescription,
@@ -45,29 +61,26 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { shortenString } from "@/utils/usedapp-external";
type PayWebhooksPageProps = {
clientId: string;
teamId: string;
teamSlug: string;
authToken: string;
projectSlug: string;
};
export function PayWebhooksPage(props: PayWebhooksPageProps) {
const webhooksQuery = useQuery({
queryFn: async () => {
return await getWebhooks({
const res = await getWebhooks({
clientId: props.clientId,
teamId: props.teamId,
authToken: props.authToken,
});
return res;
},
queryKey: ["webhooks", props.clientId, props.teamId],
});
@@ -78,11 +91,18 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
if (!webhooksQuery.data?.length) {
return (
<div className="flex flex-col items-center gap-8 rounded-lg border border-border p-8 text-center">
<h2 className="font-semibold text-xl">No webhooks configured yet.</h2>
<div className="flex flex-col items-center gap-6 rounded-lg border border-border bg-card p-8 text-center">
<div className="flex flex-col gap-2">
<h2 className="font-semibold text-xl">No webhooks configured</h2>
<p className="text-sm text-muted-foreground">
Create a webhook to receive notifications for bridge, swap or onramp
events.
</p>
</div>
<CreatePaymentWebhookButton
clientId={props.clientId}
teamId={props.teamId}
authToken={props.authToken}
>
<Button className="gap-1.5 rounded-full">
<PlusIcon className="size-4" />
@@ -95,49 +115,24 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
return (
<div>
<TableContainer>
<Table>
<TableHeader>
<TableRow>
<TableHead>Label</TableHead>
<TableHead>Url</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-center">Version</TableHead>
<TableHead className="text-center">Delete</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{webhooksQuery.data.map((webhook) => (
<TableRow key={webhook.id}>
<TableCell className="font-medium">{webhook.label}</TableCell>
<TableCell>{webhook.url}</TableCell>
<TableCell>
{formatDistanceToNow(webhook.createdAt, { addSuffix: true })}
</TableCell>
<TableCell className="text-center">
{webhook.version || "1"}
</TableCell>
<TableCell className="text-center">
<DeleteWebhookButton
clientId={props.clientId}
teamId={props.teamId}
webhookId={webhook.id}
>
<Button size="icon" variant="ghost">
<TrashIcon className="size-5" strokeWidth={1} />
</Button>
</DeleteWebhookButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<div className="space-y-4">
{webhooksQuery.data.map((webhook) => (
<WebhookCard
authToken={props.authToken}
key={webhook.id}
webhook={webhook}
clientId={props.clientId}
teamId={props.teamId}
layoutPath={`/team/${props.teamSlug}/${props.projectSlug}/webhooks/payments`}
/>
))}
</div>
<div className="mt-4 flex justify-end">
<CreatePaymentWebhookButton
clientId={props.clientId}
teamId={props.teamId}
authToken={props.authToken}
>
<Button className="gap-1.5 rounded-full">
<PlusIcon className="size-4" />
@@ -149,15 +144,105 @@ export function PayWebhooksPage(props: PayWebhooksPageProps) {
);
}
function WebhookCard(props: {
webhook: Webhook;
clientId: string;
teamId: string;
authToken: string;
layoutPath: string;
}) {
const { webhook, clientId, teamId, layoutPath } = props;
return (
<div
key={webhook.id}
className="rounded-xl border bg-card p-4 lg:p-6 relative hover:border-active-border"
>
<div className="flex mb-4">
<div className="p-2 rounded-full bg-background border">
<WebhookIcon className="size-3.5 text-muted-foreground" />
</div>
</div>
<h2 className="text-base font-medium mb-2">
<Link
href={`${layoutPath}/${webhook.id}`}
className="before:absolute before:inset-0"
>
{webhook.label || "Untitled Webhook"}
</Link>
</h2>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="ghost"
className="absolute right-4 top-4"
>
<MoreVerticalIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<EditPaymentWebhookButton
clientId={clientId}
teamId={teamId}
webhook={webhook}
authToken={props.authToken}
/>
<DeleteWebhookButton
clientId={clientId}
teamId={teamId}
webhookId={webhook.id}
authToken={props.authToken}
/>
</DropdownMenuContent>
</DropdownMenu>
<div className="space-y-1">
<div className="flex items-center gap-0.5">
<p className="text-xs text-muted-foreground">
Created{" "}
{formatDistanceToNow(webhook.createdAt, { addSuffix: true })}
</p>
{webhook.version && (
<>
<DotIcon className="size-4 text-muted-foreground" />
<p className="text-xs text-muted-foreground">
v{webhook.version || "1"}
</p>
</>
)}
</div>
<div className="flex max-w-[80%]">
<CopyTextButton
textToCopy={webhook.url}
textToShow={webhook.url}
tooltip="Copy URL"
variant="ghost"
className="text-muted-foreground text-xs -translate-x-1.5 truncate"
copyIconPosition="right"
/>
</div>
</div>
</div>
);
}
const formSchema = z.object({
label: z.string().min(3, "Label must be at least 3 characters long"),
secret: z.string().optional(),
url: z.string().url("Please enter a valid URL."),
version: z.string(),
});
function CreatePaymentWebhookButton(
props: PropsWithChildren<PayWebhooksPageProps>,
props: PropsWithChildren<{
clientId: string;
teamId: string;
authToken: string;
}>,
) {
const [open, setOpen] = useState(false);
const [secretStored, setSecretStored] = useState(false);
@@ -166,26 +251,135 @@ function CreatePaymentWebhookButton(
return randomPrivateKey();
}, [open]);
const form = useForm<z.infer<typeof formSchema>>({
defaultValues: {
label: "",
url: "",
version: "2",
},
resolver: zodResolver(formSchema),
});
const queryClient = useQueryClient();
const createMutation = useMutation({
mutationFn: async (values: z.infer<typeof formSchema>) => {
await createWebhook({
clientId: props.clientId,
async function handleSubmit(values: z.infer<typeof formSchema>) {
await createWebhook({
clientId: props.clientId,
authToken: props.authToken,
label: values.label,
secret,
teamId: props.teamId,
url: values.url,
version: Number(values.version),
});
}
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create Webhook</DialogTitle>
<DialogDescription>
Receive a webhook notification when a bridge, swap or onramp event
occurs.
</DialogDescription>
</DialogHeader>
<BridgeWebhookModalContent
type="create"
handleSubmit={handleSubmit}
secret={secret}
setOpen={setOpen}
secretStored={secretStored}
setSecretStored={setSecretStored}
clientId={props.clientId}
/>
</DialogContent>
</Dialog>
);
}
function EditPaymentWebhookButton(props: {
webhook: Webhook;
clientId: string;
teamId: string;
authToken: string;
}) {
const [open, setOpen] = useState(false);
async function handleSubmit(values: z.infer<typeof formSchema>) {
await updateWebhook({
clientId: props.clientId,
webhookId: props.webhook.id,
teamId: props.teamId,
authToken: props.authToken,
body: {
label: values.label,
secret,
teamId: props.teamId,
url: values.url,
version: Number(values.version),
});
return null;
},
});
}
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
<DropdownMenuItem
className="gap-2"
onSelect={(e) => {
e.preventDefault();
}}
>
<PencilIcon className="size-3.5" />
Edit
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Webhook</DialogTitle>
</DialogHeader>
<BridgeWebhookModalContent
type="edit"
handleSubmit={handleSubmit}
setOpen={setOpen}
clientId={props.clientId}
webhook={props.webhook}
/>
</DialogContent>
</Dialog>
);
}
function BridgeWebhookModalContent(
props: {
clientId: string;
handleSubmit: (values: z.infer<typeof formSchema>) => Promise<void>;
setOpen: (value: boolean) => void;
} & (
| {
type: "create";
secret: string;
secretStored: boolean;
setSecretStored: (value: boolean) => void;
}
| {
type: "edit";
webhook: Webhook;
}
),
) {
const queryClient = useQueryClient();
const form = useForm<z.infer<typeof formSchema>>({
defaultValues:
props.type === "create"
? {
label: "",
url: "",
version: "2",
}
: {
label: props.webhook.label,
url: props.webhook.url,
version: props.webhook.version?.toString() || "2",
},
resolver: zodResolver(formSchema),
});
const mutation = useMutation({
mutationFn: async (values: z.infer<typeof formSchema>) => {
await props.handleSubmit(values);
},
onSuccess: () => {
return queryClient.invalidateQueries({
@@ -193,137 +387,129 @@ function CreatePaymentWebhookButton(
});
},
});
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent>
<Form {...form}>
<form
className="flex flex-col gap-4"
onSubmit={form.handleSubmit((values) =>
createMutation.mutateAsync(values, {
onError: (err) => {
toast.error("Failed to create webhook", {
description: err instanceof Error ? err.message : undefined,
});
},
onSuccess: () => {
setOpen(false);
setSecretStored(false);
toast.success("Webhook created successfully");
form.reset();
form.clearErrors();
form.setValue("url", "");
form.setValue("label", "");
form.setValue("version", "2");
form.setValue("secret", undefined);
},
}),
)}
>
<DialogHeader>
<DialogTitle>Create Webhook</DialogTitle>
<DialogDescription>
Receive a webhook notification when a bridge, swap or onramp
event occurs.
</DialogDescription>
</DialogHeader>
<FormField
name="url"
render={({ field }) => (
<FormItem>
<RequiredFormLabel>URL</RequiredFormLabel>
<Input {...field} placeholder="https://" />
<FormDescription>
This is the URL that will receive the webhook.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="label"
render={({ field }) => (
<FormItem>
<RequiredFormLabel>Label</RequiredFormLabel>
<Input {...field} placeholder="My Webhook" />
<FormDescription>
A label to help you identify this webhook.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="version"
render={({ field }) => (
<FormItem>
<FormLabel>Version</FormLabel>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger className="w-full">
<SelectValue placeholder="v2" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">v2</SelectItem>
<SelectItem value="1">v1</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Select the data format of the webhook payload (v2
recommended, v1 for legacy users).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<section>
<FormLabel>Webhook Secret</FormLabel>
<CopyTextButton
className="!h-auto my-1 w-full justify-between truncate bg-card px-3 py-3 font-mono"
copyIconPosition="right"
textToCopy={secret}
textToShow={shortenString(secret)}
tooltip="Copy Webhook Secret"
/>
<Form {...form}>
<form
className="flex flex-col gap-4"
onSubmit={form.handleSubmit((values) =>
mutation.mutateAsync(values, {
onError: (err) => {
toast.error("Failed to create webhook", {
description: err instanceof Error ? err.message : undefined,
});
},
onSuccess: () => {
props.setOpen(false);
toast.success(
`Webhook ${props.type === "create" ? "created" : "updated"} successfully`,
);
},
}),
)}
>
<FormField
name="url"
render={({ field }) => (
<FormItem>
<RequiredFormLabel>URL</RequiredFormLabel>
<Input {...field} placeholder="https://" autoComplete="off" />
<FormDescription>
Passed as a bearer token in all webhook requests to verify the
authenticity of the request.
This is the URL that will receive the webhook.
</FormDescription>
<CheckboxWithLabel className="my-2 text-foreground">
<Checkbox
checked={secretStored}
onCheckedChange={(v) => {
setSecretStored(!!v);
}}
/>
I confirm that I've securely stored my webhook secret
</CheckboxWithLabel>
</section>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="label"
render={({ field }) => (
<FormItem>
<RequiredFormLabel>Label</RequiredFormLabel>
<Input {...field} placeholder="My Webhook" />
<FormDescription>
A label to help you identify this webhook.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
className="gap-2"
disabled={createMutation.isPending || !secretStored}
type="submit"
>
Create Webhook
{createMutation.isPending && <Spinner className="size-4" />}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
<FormField
name="version"
render={({ field }) => (
<FormItem>
<FormLabel>Version</FormLabel>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger className="w-full">
<SelectValue placeholder="v2" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">v2</SelectItem>
<SelectItem value="1">v1</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Select the data format of the webhook payload (v2 recommended,
v1 for legacy users).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{props.type === "create" && (
<section>
<FormLabel>Webhook Secret</FormLabel>
<CopyTextButton
className="!h-auto my-1 w-full justify-between truncate bg-card px-3 py-3 font-mono"
copyIconPosition="right"
textToCopy={props.secret}
textToShow={shortenString(props.secret)}
tooltip="Copy Webhook Secret"
/>
<FormDescription>
Passed as a bearer token in all webhook requests to verify the
authenticity of the request.
</FormDescription>
<CheckboxWithLabel className="my-2 text-foreground">
<Checkbox
checked={!!props.secretStored}
onCheckedChange={(v) => {
props.setSecretStored(!!v);
}}
/>
I confirm that I've securely stored my webhook secret
</CheckboxWithLabel>
</section>
)}
<DialogFooter>
<Button
className="gap-2"
disabled={
mutation.isPending ||
(props.type === "create" && !props.secretStored)
}
type="submit"
>
{props.type === "create" ? "Create Webhook" : "Update Webhook"}
{mutation.isPending && <Spinner className="size-4" />}
</Button>
</DialogFooter>
</form>
</Form>
);
}
function DeleteWebhookButton(
props: PropsWithChildren<PayWebhooksPageProps & { webhookId: string }>,
) {
function DeleteWebhookButton(props: {
clientId: string;
teamId: string;
webhookId: string;
authToken: string;
}) {
const [open, setOpen] = useState(false);
const queryClient = useQueryClient();
const deleteMutation = useMutation({
@@ -332,8 +518,8 @@ function DeleteWebhookButton(
clientId: props.clientId,
teamId: props.teamId,
webhookId: id,
authToken: props.authToken,
});
return null;
},
onSuccess: () => {
return queryClient.invalidateQueries({
@@ -343,7 +529,17 @@ function DeleteWebhookButton(
});
return (
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogTrigger asChild>
<DropdownMenuItem
className="gap-2 text-destructive"
onSelect={(e) => {
e.preventDefault();
}}
>
<TrashIcon className="size-3.5" />
Delete
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
@@ -27,6 +27,7 @@ interface PayConfigProps {
teamSlug: string;
fees: Fee;
projectWalletAddress?: string;
authToken: string;
}
export const PayConfig: React.FC<PayConfigProps> = (props) => {
@@ -48,6 +49,7 @@ export const PayConfig: React.FC<PayConfigProps> = (props) => {
feeBps: values.developerFeeBPS,
feeRecipient: values.payoutAddress,
teamId: props.teamId,
authToken: props.authToken,
});
},
});
@@ -37,6 +37,7 @@ export default async function Page(props: {
let fees = await getFees({
clientId: project.publishableKey,
teamId: team.id,
authToken: authToken,
}).catch(() => {
return {
createdAt: "",
@@ -69,6 +70,7 @@ export default async function Page(props: {
teamId={team.id}
projectWalletAddress={projectWallet?.address}
teamSlug={team_slug}
authToken={authToken}
/>
</div>
);
@@ -560,6 +560,98 @@ export async function createWalletAccessToken(props: {
],
type: "eoa:create",
},
{
metadataPatterns: [
{
key: "projectId",
rule: {
pattern: props.project.id,
},
},
{
key: "teamId",
rule: {
pattern: props.project.teamId,
},
},
{
key: "type",
rule: {
pattern: "server-wallet",
},
},
],
type: "solana:read",
},
{
requiredMetadataPatterns: [
{
key: "projectId",
rule: {
pattern: props.project.id,
},
},
{
key: "teamId",
rule: {
pattern: props.project.teamId,
},
},
{
key: "type",
rule: {
pattern: "server-wallet",
},
},
],
type: "solana:create",
},
{
metadataPatterns: [
{
key: "projectId",
rule: {
pattern: props.project.id,
},
},
{
key: "teamId",
rule: {
pattern: props.project.teamId,
},
},
{
key: "type",
rule: {
pattern: "server-wallet",
},
},
],
type: "solana:signTransaction",
},
{
metadataPatterns: [
{
key: "projectId",
rule: {
pattern: props.project.id,
},
},
{
key: "teamId",
rule: {
pattern: props.project.teamId,
},
},
{
key: "type",
rule: {
pattern: "server-wallet",
},
},
],
type: "solana:signMessage",
},
],
},
},
@@ -633,6 +725,52 @@ async function createManagementAccessToken(props: {
],
type: "eoa:create",
},
{
metadataPatterns: [
{
key: "projectId",
rule: {
pattern: props.project.id,
},
},
{
key: "teamId",
rule: {
pattern: props.project.teamId,
},
},
{
key: "type",
rule: {
pattern: "server-wallet",
},
},
],
type: "solana:read",
},
{
requiredMetadataPatterns: [
{
key: "projectId",
rule: {
pattern: props.project.id,
},
},
{
key: "teamId",
rule: {
pattern: props.project.teamId,
},
},
{
key: "type",
rule: {
pattern: "server-wallet",
},
},
],
type: "solana:create",
},
{
metadataPatterns: [
{
@@ -0,0 +1,283 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import type {
getWebhookSends,
WebhookSend,
} from "@/api/universal-bridge/developer";
import { WebhookSendsUI } from "./webhook-sends";
const meta = {
title: "app/webhooks/WebhookSendsUI",
component: WebhookSendsUI,
decorators: [
(Story) => (
<div className="container max-w-7xl py-10">
<Story />
</div>
),
],
} satisfies Meta<typeof WebhookSendsUI>;
export default meta;
type Story = StoryObj<typeof meta>;
type WebhookSendResult = Awaited<ReturnType<typeof getWebhookSends>>;
function createMockWebhookSendItem(): WebhookSend {
const type = Math.random() > 0.5 ? "onramp" : "bridge";
const status =
Math.random() > 0.5
? "PENDING"
: Math.random() > 0.5
? "COMPLETED"
: "FAILED";
const responseStatus =
Math.random() > 0.5 ? (Math.random() > 0.5 ? 200 : 500) : 404;
const responseType = Math.random() > 0.5 ? "json" : "text";
return {
id: crypto.randomUUID(),
webhookId: "8fa29fe7-95a9-4b70-97a6-7e80504f5a75",
webhookUrl: "https://example.webhook/foo/bar",
paymentId:
"0x9620fef2c2267b223b8e998b1453b15324acf3ac03e531c9b60c38d6a02843cb",
transactionId:
type === "bridge"
? "0x79fa097789678db4fcb8712d49133a9447e0039c8a8f93be0cfe3664ac304b80"
: null,
onrampId: type === "onramp" ? crypto.randomUUID() : null,
status: status,
body: {
version: 1,
type: type === "bridge" ? "pay.onchain-transaction" : undefined,
data:
type === "bridge"
? {
buyWithCryptoStatus: {
quote: {
fromToken: {
chainId: 42161,
tokenAddress: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
decimals: 18,
priceUSDCents: 452169,
name: "Ether",
symbol: "ETH",
},
toToken: {
chainId: 8453,
tokenAddress: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
decimals: 18,
priceUSDCents: 451824,
name: "Ether",
symbol: "ETH",
},
fromAmountWei: "101000000000000",
fromAmount: "0.000101",
toAmountWei: "100000000000000",
toAmount: "0.0001",
toAmountMin: "0.0001",
toAmountMinWei: "100000000000000",
estimated: {
fromAmountUSDCents: 46,
toAmountMinUSDCents: 45,
toAmountUSDCents: 45,
slippageBPS: 0,
feesUSDCents: 0,
gasCostUSDCents: 0,
durationSeconds: 60,
},
createdAt: "2025-09-16T23:20:53.228Z",
},
status: "PENDING",
subStatus: "NONE",
fromAddress: "0x2a4f24f935eb178e3e7ba9b53a5ee6d8407c0709",
toAddress: "0x2a4f24f935eb178e3e7ba9b53a5ee6d8407c0709",
bridge: "Universal Bridge",
},
}
: {
buyWithFiatStatus: {
intentId: "9833815f-77b8-4e3e-b0cf-b8741be16ede",
fromAddress: "0x0000000000000000000000000000000000000000",
toAddress: "0xEbE5C4774fd4eEA444094367E01d26f0771e248b",
status: "PENDING",
quote: {
createdAt: "2025-06-19T18:32:42.092Z",
estimatedOnRampAmount: "0.002",
estimatedOnRampAmountWei: "2000000000000000",
estimatedToTokenAmount: "0.002",
estimatedToTokenAmountWei: "2000000000000000",
onRampToken: {
chainId: 42161,
address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
symbol: "ETH",
name: "Ether",
decimals: 18,
priceUsd: 2511.635965,
iconUri: "https://assets.relay.link/icons/1/light.png",
},
toToken: {
chainId: 42161,
address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
symbol: "ETH",
name: "Ether",
decimals: 18,
priceUsd: 2511.635965,
iconUri: "https://assets.relay.link/icons/1/light.png",
},
estimatedDurationSeconds: 60,
},
failureMessage: "",
purchaseData: null,
},
},
},
success: true,
responseStatus: responseStatus,
response:
responseType === "json"
? '{ "message": "Success" }'
: "This is a plaintext response",
createdAt: "2025-09-16T23:20:53.981Z",
};
}
function createMockWebhookSendResult(length: number): WebhookSendResult {
return {
data: Array.from({ length }, createMockWebhookSendItem),
pagination: {
limit: 10,
offset: 0,
total: 100,
},
};
}
const mockEmptyWebhookSendResult: WebhookSendResult = {
data: [],
pagination: {
limit: 10,
offset: 0,
total: 0,
},
};
export const Loading: Story = {
args: {
webhookId: "mock-webhook-id",
authToken: "mock-auth-token",
projectClientId: "mock-project-id",
teamId: "mock-team-id",
getWebhookSends: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000000));
return createMockWebhookSendResult(1);
},
resendWebhook: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return true;
},
},
};
export const TenResults: Story = {
args: {
webhookId: "mock-webhook-id",
authToken: "mock-auth-token",
projectClientId: "mock-project-id",
teamId: "mock-team-id",
getWebhookSends: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return createMockWebhookSendResult(10);
},
resendWebhook: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return true;
},
},
};
export const OneResult: Story = {
args: {
webhookId: "mock-webhook-id",
authToken: "mock-auth-token",
projectClientId: "mock-project-id",
teamId: "mock-team-id",
getWebhookSends: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return createMockWebhookSendResult(1);
},
resendWebhook: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return true;
},
},
};
export const FewResults: Story = {
args: {
webhookId: "mock-webhook-id",
authToken: "mock-auth-token",
projectClientId: "mock-project-id",
teamId: "mock-team-id",
getWebhookSends: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return createMockWebhookSendResult(3);
},
resendWebhook: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return true;
},
},
};
export const NoResults: Story = {
args: {
webhookId: "mock-webhook-id",
authToken: "mock-auth-token",
projectClientId: "mock-project-id",
teamId: "mock-team-id",
getWebhookSends: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return mockEmptyWebhookSendResult;
},
resendWebhook: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return true;
},
},
};
export const ErrorState: Story = {
args: {
webhookId: "mock-webhook-id",
authToken: "mock-auth-token",
projectClientId: "mock-project-id",
teamId: "mock-team-id",
getWebhookSends: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
throw new Error("Failed to fetch webhook sends");
},
resendWebhook: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return true;
},
},
};
export const ResendError: Story = {
args: {
webhookId: "mock-webhook-id",
authToken: "mock-auth-token",
projectClientId: "mock-project-id",
teamId: "mock-team-id",
getWebhookSends: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return createMockWebhookSendResult(1);
},
resendWebhook: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
throw new Error("Failed to resend webhook");
},
},
};
@@ -0,0 +1,83 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { getAuthToken } from "@/api/auth-token";
import { getProject } from "@/api/project/projects";
import { getWebhookById } from "@/api/universal-bridge/developer";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { WebhookSends } from "./webhook-sends";
export default async function Page(props: {
params: Promise<{
team_slug: string;
project_slug: string;
id: string;
}>;
}) {
const [authToken, params] = await Promise.all([getAuthToken(), props.params]);
const project = await getProject(params.team_slug, params.project_slug);
if (!project || !authToken) {
notFound();
}
const webhook = await getWebhookById({
clientId: project.publishableKey,
teamId: project.teamId,
authToken,
webhookId: params.id,
});
if (!webhook) {
notFound();
}
return (
<div>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link
href={`/team/${params.team_slug}/${params.project_slug}/webhooks/payments`}
>
Webhooks
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
</BreadcrumbList>
</Breadcrumb>
<div className="h-4" />
<div className="space-y-0.5 border-b border-dashed pb-6 overflow-hidden ">
<h1 className="text-xl font-semibold tracking-tight">
{webhook.label || "Untitled Webhook"}
</h1>
<CopyTextButton
textToCopy={webhook.url}
textToShow={webhook.url}
tooltip="Copy URL"
variant="ghost"
className="text-muted-foreground text-sm -translate-x-1.5 truncate max-w-full"
copyIconPosition="right"
/>
</div>
<WebhookSends
webhookId={params.id}
authToken={authToken}
projectClientId={project.publishableKey}
teamId={project.teamId}
/>
</div>
);
}
@@ -0,0 +1,490 @@
"use client";
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
import { PlainTextCodeBlock } from "@workspace/ui/components/code/plaintext-code";
import { cn } from "@workspace/ui/lib/utils";
import { formatDate } from "date-fns";
import {
ChevronRightIcon,
CircleAlertIcon,
CircleCheckIcon,
DotIcon,
RotateCwIcon,
} from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import {
getWebhookSends,
resendWebhook,
type WebhookSend,
} from "@/api/universal-bridge/developer";
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { CodeClient } from "@/components/ui/code/code.client";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Spinner } from "@/components/ui/Spinner";
import { Skeleton } from "@/components/ui/skeleton";
import { useIsMobile } from "@/hooks/use-mobile";
const pageSize = 10;
type WebhookSendsProps = {
webhookId: string;
authToken: string;
projectClientId: string;
teamId: string;
};
export function WebhookSends(props: WebhookSendsProps) {
return (
<WebhookSendsUI
{...props}
getWebhookSends={getWebhookSends}
resendWebhook={resendWebhook}
/>
);
}
export function WebhookSendsUI(
props: WebhookSendsProps & {
getWebhookSends: typeof getWebhookSends;
resendWebhook: typeof resendWebhook;
},
) {
const [page, setPage] = useState(1);
const [_selectedWebhookSend, setSelectedWebhookSend] =
useState<WebhookSend | null>(null);
const [successFilter, _setSuccessFilter] = useState<
"all" | "success" | "failed"
>("all");
const isMobile = useIsMobile();
const [_isDialogOpen, setIsDialogOpen] = useState(false);
const isDialogOpen = isMobile && _isDialogOpen;
const webhookSendsQuery = useQuery({
queryKey: ["webhook-sends", props.webhookId, page],
retry: false,
refetchOnWindowFocus: false,
queryFn: () =>
props.getWebhookSends({
webhookId: props.webhookId,
authToken: props.authToken,
projectClientId: props.projectClientId,
teamId: props.teamId,
limit: pageSize,
offset: (page - 1) * pageSize,
success:
successFilter === "all" ? undefined : successFilter === "success",
}),
// preserve the previous data when the page changes
placeholderData: keepPreviousData,
});
const totalItems = webhookSendsQuery.data?.pagination.total;
const totalPages = totalItems ? Math.ceil(totalItems / pageSize) : 1;
const selectedWebhookSend =
_selectedWebhookSend || (isMobile ? null : webhookSendsQuery.data?.data[0]);
return (
<div className="pt-4">
<div className="pb-4 border-b">
<h2 className="text-lg font-semibold">Events</h2>
{/* show a filter selector here */}
</div>
{webhookSendsQuery.isError ? (
<div className="text-muted-foreground text-base py-20 flex items-center justify-center border-b">
Failed to load events
</div>
) : (
<div className="border-b">
{/* grid */}
<div className="flex flex-col gap-2 lg:grid lg:grid-cols-[1fr_750px] ">
{/* left */}
<div>
{webhookSendsQuery.isFetching &&
Array.from({ length: pageSize }).map(() => (
// biome-ignore lint/correctness/useJsxKeyInIterable: ok
<WebhookSendCompactInfoSkeleton />
))}
{!webhookSendsQuery.isFetching &&
webhookSendsQuery.data?.data.map((webhookSend) => (
<WebhookSendCompactInfo
key={webhookSend.id}
webhookSend={webhookSend}
isSelected={selectedWebhookSend?.id === webhookSend.id}
onSelect={() => {
setSelectedWebhookSend(webhookSend);
setIsDialogOpen(true);
}}
/>
))}
{!webhookSendsQuery.isFetching &&
webhookSendsQuery.data?.data.length === 0 && (
<div className="text-muted-foreground text-base py-20 flex items-center justify-center">
No events found
</div>
)}
</div>
{/* right - desktop */}
<div className="hidden lg:block lg:pl-4 lg:border-l lg:pt-4">
{webhookSendsQuery.isFetching ? (
<div className="h-full flex justify-center items-center">
<Spinner className="size-10" />
</div>
) : selectedWebhookSend ? (
<WebhookSendInfo
webhookSend={selectedWebhookSend}
authToken={props.authToken}
projectClientId={props.projectClientId}
teamId={props.teamId}
resendWebhook={props.resendWebhook}
/>
) : null}
</div>
{/* Dialog for mobile */}
<Dialog
open={isDialogOpen}
onOpenChange={(open) => {
setIsDialogOpen(open);
if (!open) {
setSelectedWebhookSend(null);
}
}}
>
<DialogContent className="max-h-[90vh] overflow-y-auto p-4">
{selectedWebhookSend && (
<WebhookSendInfo
webhookSend={selectedWebhookSend}
authToken={props.authToken}
projectClientId={props.projectClientId}
teamId={props.teamId}
resendWebhook={props.resendWebhook}
/>
)}
</DialogContent>
</Dialog>
</div>
{totalPages && totalPages > 1 && (
<div className="py-5 border-t">
<PaginationButtons
activePage={page}
totalPages={totalPages}
onPageClick={(page) => {
setPage(page);
setSelectedWebhookSend(null);
}}
/>
</div>
)}
</div>
)}
</div>
);
}
function WebhookSendInfo(props: {
webhookSend: WebhookSend;
authToken: string;
projectClientId: string;
teamId: string;
resendWebhook: typeof resendWebhook;
}) {
const bodyString = useMemo(() => {
try {
if (!props.webhookSend.body) {
return undefined;
}
if (
typeof props.webhookSend.body === "object" &&
props.webhookSend.body !== null
) {
return JSON.stringify(props.webhookSend.body, null, 2);
}
return undefined;
} catch {
return undefined;
}
}, [props.webhookSend.body]);
const responseString: { lang: "json" | "text"; code: string } =
useMemo(() => {
if (!props.webhookSend.response) {
return {
lang: "text",
code: "",
};
}
try {
const json = JSON.parse(props.webhookSend.response);
return {
lang: "json",
code: JSON.stringify(json, null, 2),
};
} catch {
return {
lang: "text",
code: props.webhookSend.response,
};
}
}, [props.webhookSend.response]);
const resendMutation = useMutation({
mutationFn: async () => {
if (props.webhookSend.onrampId) {
const result = await props.resendWebhook({
onrampId: props.webhookSend.onrampId,
authToken: props.authToken,
projectClientId: props.projectClientId,
teamId: props.teamId,
});
return result;
} else if (props.webhookSend.paymentId) {
const result = await props.resendWebhook({
paymentId: props.webhookSend.paymentId,
authToken: props.authToken,
projectClientId: props.projectClientId,
teamId: props.teamId,
});
return result;
} else {
throw new Error("No transaction or onramp ID found");
}
},
onSuccess: () => {
toast.success("Webhook resent");
},
onError: (error: unknown) => {
toast.error("Failed to resend webhook", {
description: error instanceof Error ? error.message : undefined,
});
},
});
return (
<div className="pb-6 relative fade-in-0 duration-300 animate-in flex flex-col min-w-0">
<div className="pr-10">
<h3 className="text-xl font-semibold mb-1">
<span>{getWebhookSendTitle(props.webhookSend)}.</span>
<span>{props.webhookSend.status.toLowerCase()}</span>
</h3>
<p className="text-muted-foreground text-sm">
Sent on{" "}
{formatDate(
new Date(props.webhookSend.createdAt),
"MMM d, yyyy HH:mm:ss",
)}
</p>
</div>
<Button
variant="outline"
size="sm"
className="lg:absolute lg:top-0 lg:right-0 gap-2 rounded-lg bg-card mt-4 lg:mt-0"
onClick={() => resendMutation.mutate()}
>
{resendMutation.isPending ? (
<Spinner className="size-4" />
) : (
<RotateCwIcon className="size-3.5 text-muted-foreground" />
)}
Resend
</Button>
<div className="h-4" />
<div className="space-y-4">
{/* succes */}
<div>
<h3 className="text-sm mb-0.5 font-medium">Delivery Status</h3>
<p className="text-muted-foreground text-sm">
{" "}
{props.webhookSend.success ? "Delivered" : "Failed"}{" "}
</p>
</div>
{/* transaction id */}
{props.webhookSend.transactionId && (
<div>
<h3 className="text-sm mb-0.5 font-medium">Transaction ID</h3>
<CopyTextButton
copyIconPosition="right"
textToCopy={props.webhookSend.transactionId}
textToShow={props.webhookSend.transactionId}
tooltip="Copy Transaction ID"
variant="ghost"
className="text-muted-foreground text-sm -translate-x-1.5 truncate max-w-full"
/>
</div>
)}
{/* Payment ID */}
{props.webhookSend.paymentId && (
<CopyTextField
label="Payment ID"
value={props.webhookSend.paymentId}
/>
)}
{/* onramp id */}
{props.webhookSend.onrampId && (
<CopyTextField label="Onramp ID" value={props.webhookSend.onrampId} />
)}
{/* event id */}
{props.webhookSend.id && (
<CopyTextField label="Event ID" value={props.webhookSend.id} />
)}
{/* payload */}
<div>
<h3 className="text-sm mb-2 font-medium">Payload</h3>
<CodeClient
code={bodyString || ""}
lang="json"
scrollableClassName="max-h-[350px]"
className="[&_code]:text-xs"
/>
</div>
{/* response */}
<div>
<div className="flex items-center gap-2 mb-2">
<h3 className="text-sm font-medium">Response</h3>
<DotIcon className="size-3.5 text-muted-foreground hidden" />
<StatusBadge status={props.webhookSend.responseStatus} />
</div>
{responseString.lang === "text" ? (
<PlainTextCodeBlock
code={responseString.code}
scrollableClassName="max-h-[350px]"
codeClassName="whitespace-pre-wrap text-xs"
/>
) : (
<CodeClient
code={responseString.code}
lang="json"
scrollableClassName="max-h-[350px]"
className="[&_code]:text-xs"
/>
)}
</div>
</div>
</div>
);
}
function CopyTextField(props: { label: string; value: string }) {
return (
<div>
<h3 className="text-sm mb-0.5 font-medium"> {props.label}</h3>
<CopyTextButton
copyIconPosition="right"
textToCopy={props.value}
textToShow={props.value}
tooltip={`Copy ${props.label}`}
variant="ghost"
className="text-muted-foreground text-sm -translate-x-1.5 truncate max-w-full"
/>
</div>
);
}
function WebhookSendCompactInfo(props: {
webhookSend: WebhookSend;
isSelected: boolean;
onSelect: (webhookSend: WebhookSend) => void;
}) {
return (
<div className="py-2 border-b last:border-b-0 fade-in-0 duration-300 animate-in">
<Button
variant="ghost"
className={cn(
"flex items-center gap-3 py-2.5 px-4 h-auto w-full justify-start text-left hover:bg-card rounded-lg",
props.isSelected && "!bg-accent/70",
)}
onClick={() => props.onSelect(props.webhookSend)}
>
<StatusBadge status={props.webhookSend.responseStatus} />
<div className="space-y-1">
<h3 className="text-sm text-foreground">
<span>{getWebhookSendTitle(props.webhookSend)}.</span>
<span>{props.webhookSend.status.toLowerCase()}</span>
</h3>
<p className="text-xs text-muted-foreground">
{formatDate(
new Date(props.webhookSend.createdAt),
"MMM d, yyyy HH:mm:ss",
)}
</p>
</div>
<ChevronRightIcon
className={cn(
"size-3.5 ml-auto text-muted-foreground opacity-0 transition-opacity",
props.isSelected && "opacity-100",
)}
/>
</Button>
</div>
);
}
function WebhookSendCompactInfoSkeleton() {
return (
<div className="h-[77px] flex gap-3 items-center border-b px-4 last:border-b-0">
<Skeleton className="w-[45px] h-[22px] rounded-full" />
<div className="space-y-1">
<Skeleton className="w-[240px] h-4" />
<Skeleton className="w-[150px] h-3" />
</div>
</div>
);
}
function getWebhookSendTitle(webhookSend: WebhookSend) {
if (typeof webhookSend.body === "object" && webhookSend.body !== null) {
if ("type" in webhookSend.body) {
const type = webhookSend.body.type;
if (typeof type === "string") {
return type;
}
}
}
if (webhookSend.onrampId) {
return "onramp";
}
return "unknown";
}
function StatusBadge(props: { status: number }) {
const variant =
props.status >= 200 && props.status < 300 ? "success" : "destructive";
return (
<Badge variant={variant} className="px-1.5 gap-1.5">
{variant === "success" ? (
<CircleCheckIcon className="size-3 opacity-70" />
) : (
<CircleAlertIcon className="size-3 opacity-70" />
)}
{props.status}
</Badge>
);
}
@@ -21,6 +21,9 @@ export default async function Page(props: {
<PayWebhooksPage
clientId={project.publishableKey}
teamId={project.teamId}
teamSlug={params.team_slug}
projectSlug={params.project_slug}
authToken={authToken}
/>
);
}
@@ -1,36 +0,0 @@
"use client";
import { useTheme } from "next-themes";
import { DarkVeil } from "@/components/DarkVeil";
const bgBackground: Record<string, [number, number, number]> = {
dark: [0, 0, 0],
light: [250 / 255, 250 / 255, 250 / 255],
};
const hues = {
light: 65,
dark: -85,
};
const patternLightnesss = {
light: -1,
dark: 0.8,
};
export function BridgeVeil() {
const theme = useTheme();
const isLight = theme.theme === "light";
const patternLightness = isLight
? patternLightnesss.light
: patternLightnesss.dark;
const hueShift = isLight ? hues.light : hues.dark;
return (
<DarkVeil
backgroundColor={bgBackground[isLight ? "light" : "dark"]}
patternLightness={patternLightness}
hueShift={hueShift}
warpAmount={0.5}
/>
);
}
@@ -6,7 +6,7 @@ import { ThirdwebMiniLogo } from "../../(app)/components/ThirdwebMiniLogo";
export function PageHeader(props: { containerClassName?: string }) {
return (
<div className="border-b border-border/50 border-dashed">
<div className="border-b border-dashed">
<header
className={cn(
"container flex max-w-7xl justify-between py-4",
+8 -11
View File
@@ -1,4 +1,5 @@
import { cn } from "@workspace/ui/lib/utils";
import { DotIcon } from "lucide-react";
import type { Metadata } from "next";
import type { Address } from "thirdweb";
import { defineChain } from "thirdweb/chains";
@@ -6,7 +7,6 @@ import { getContract } from "thirdweb/contract";
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
import { FaqSection } from "@/components/blocks/faq-section";
import { AppFooter } from "@/components/footers/app-footer";
import { BridgeVeil } from "./components/BridgeVeil";
import { PillLink } from "./components/client/pill-link";
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
import { PageHeader } from "./components/header";
@@ -55,30 +55,27 @@ export default async function BridgePage({
return (
<div className="grow flex flex-col relative overflow-hidden">
<div className="absolute top-0 left-0 right-0 h-dvh md:h-[1200px] fade-in-0 animate-in duration-700">
<BridgeVeil />
</div>
<div className="relative z-10">
<PageHeader />
<div className="h-20" />
<div className="h-12 lg:h-20" />
<div className="relative z-10">
<div className="container">
<h1 className="text-5xl md:text-6xl font-bold mb-6 tracking-tighter text-center leading-none text-pretty">
<h1 className="text-3xl md:text-6xl font-bold mb-6 tracking-tighter text-center leading-8 lg:leading-none text-balance">
Bridge and Swap tokens <br className="max-sm:hidden" /> across any
chain, instantly
</h1>
<div className="flex-wrap flex gap-3 items-center justify-center">
<div className="flex flex-wrap gap-2 items-center justify-center">
<DataPill>85+ Chains Supported</DataPill>
<DotIcon className="size-3 text-muted-foreground lg:hidden" />
<DataPill>4500+ Tokens Supported</DataPill>
<DataPill>9+ Million Routes Available</DataPill>
</div>
</div>
<div className="h-16" />
<div className="h-12 lg:h-16" />
<div className="flex grow items-center justify-center px-4 relative">
<DotsBackgroundPattern />
@@ -134,7 +131,7 @@ export default async function BridgePage({
function DataPill(props: { children: React.ReactNode }) {
return (
<p className="flex items-center gap-1.5 text-foreground/50 text-xs bg-accent/30 backdrop-blur-lg border border-border/70 rounded-full px-3 py-1.5 hover:text-foreground transition-colors duration-300">
<p className="lg:bg-card flex items-center gap-1.5 text-muted-foreground text-xs lg:backdrop-blur-lg lg:border lg:border-border/70 rounded-full lg:px-3 lg:py-1.5 hover:text-foreground transition-colors duration-300">
{props.children}
</p>
);
@@ -144,7 +141,7 @@ function DotsBackgroundPattern(props: { className?: string }) {
return (
<div
className={cn(
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-pink-700/30 dark:text-pink-500/20",
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/20 hidden lg:block",
props.className,
)}
style={{
@@ -4,12 +4,14 @@ import { convertToModelMessages, streamText, type UIMessage } from "ai";
// Allow streaming responses up to 5 minutes
export const maxDuration = 300;
const thirdwebAI = createThirdwebAI({
baseURL: `https://${process.env.NEXT_PUBLIC_API_URL}`,
secretKey: process.env.THIRDWEB_SECRET_KEY,
});
export async function POST(req: Request) {
const SECRET_KEY = process.env.THIRDWEB_SECRET_KEY as string;
const thirdwebAI = createThirdwebAI({
baseURL: `https://${process.env.NEXT_PUBLIC_API_URL}`,
secretKey: SECRET_KEY,
});
const body = await req.json();
const { messages, id }: { messages: UIMessage[]; id: string } = body;
@@ -7,25 +7,27 @@ import { token } from "../../payments/x402/components/constants";
// Allow streaming responses up to 5 minutes
export const maxDuration = 300;
const client = createThirdwebClient({
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
});
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string;
const ENGINE_VAULT_ACCESS_TOKEN = process.env
.ENGINE_VAULT_ACCESS_TOKEN as string;
// const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
const API_URL = "http://localhost:3030";
const twFacilitator = facilitator({
baseUrl: `${API_URL}/v1/payments/x402`,
client,
serverWalletAddress: BACKEND_WALLET_ADDRESS,
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
});
export async function GET(request: NextRequest) {
const SECRET_KEY = process.env.THIRDWEB_SECRET_KEY as string;
const client = createThirdwebClient({
secretKey: SECRET_KEY,
});
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string;
const ENGINE_VAULT_ACCESS_TOKEN = process.env
.ENGINE_VAULT_ACCESS_TOKEN as string;
// const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
const API_URL = "http://localhost:3030";
const twFacilitator = facilitator({
baseUrl: `${API_URL}/v1/payments/x402`,
client,
serverWalletAddress: BACKEND_WALLET_ADDRESS,
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
});
const paymentData = request.headers.get("X-PAYMENT");
const queryParams = request.nextUrl.searchParams;
+8
View File
@@ -1,5 +1,13 @@
# wagmi-inapp
## 0.0.9
### Patch Changes
- Updated dependencies [[`8280efb`](https://github.com/thirdweb-dev/js/commit/8280efb99189c5b849156df383522f9e1b8f2c52), [`464af6a`](https://github.com/thirdweb-dev/js/commit/464af6a54cb776158a1656e793ee181c2c221f27)]:
- thirdweb@5.108.14
- @thirdweb-dev/wagmi-adapter@0.2.164
## 0.0.8
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wagmi-inapp",
"private": true,
"version": "0.0.8",
"version": "0.0.9",
"type": "module",
"scripts": {
"dev": "vite",
+7
View File
@@ -1,5 +1,12 @@
# @thirdweb-dev/nebula
## 0.2.67
### Patch Changes
- Updated dependencies [[`8280efb`](https://github.com/thirdweb-dev/js/commit/8280efb99189c5b849156df383522f9e1b8f2c52), [`464af6a`](https://github.com/thirdweb-dev/js/commit/464af6a54cb776158a1656e793ee181c2c221f27)]:
- thirdweb@5.108.14
## 0.2.66
### Patch Changes
+1 -1
View File
@@ -57,5 +57,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.66"
"version": "0.2.67"
}
+8
View File
@@ -1,5 +1,13 @@
# thirdweb
## 5.108.14
### Patch Changes
- [#8253](https://github.com/thirdweb-dev/js/pull/8253) [`8280efb`](https://github.com/thirdweb-dev/js/commit/8280efb99189c5b849156df383522f9e1b8f2c52) Thanks [@joaquim-verges](https://github.com/joaquim-verges)! - Fix cb wallet transaction desktop popup when logged in via mobile QR code
- [#8235](https://github.com/thirdweb-dev/js/pull/8235) [`464af6a`](https://github.com/thirdweb-dev/js/commit/464af6a54cb776158a1656e793ee181c2c221f27) Thanks [@kumaryash90](https://github.com/kumaryash90)! - CLI updates for stylus - new templates
## 5.108.13
### Patch Changes
+1 -1
View File
@@ -430,5 +430,5 @@
}
},
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "5.108.13"
"version": "5.108.14"
}
@@ -58,6 +58,8 @@ export async function createStylusProject() {
{ title: "Airdrop ERC20", value: "airdrop20" },
{ title: "Airdrop ERC721", value: "airdrop721" },
{ title: "Airdrop ERC1155", value: "airdrop1155" },
{ title: "ZK ERC721", value: "zk-erc721" },
{ title: "ZK ERC20", value: "zk-erc20" },
],
message: "Select a template:",
name: "projectType",
@@ -119,6 +121,18 @@ export async function createStylusProject() {
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
stdio: "inherit",
});
} else if (projectType === "zk-erc721") {
const repoUrl = "[email protected]:thirdweb-example/stylus-zk-erc721.git";
spinner.start(`Creating new ZK ERC721 Stylus project: ${projectName}...`);
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
stdio: "inherit",
});
} else if (projectType === "zk-erc20") {
const repoUrl = "[email protected]:thirdweb-example/stylus-zk-erc20.git";
spinner.start(`Creating new ZK ERC20 Stylus project: ${projectName}...`);
newProject = spawnSync("git", ["clone", repoUrl, projectName], {
stdio: "inherit",
});
}
if (!newProject || newProject.status !== 0) {
@@ -44,7 +44,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFT", () => {
"uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1",
},
"owner": null,
"tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
"tokenAddress": "0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e",
"tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1",
"type": "ERC721",
}
@@ -13,7 +13,6 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFTs", () => {
});
expect(nfts.length).toBe(5);
// TODO (insight): re-enable once insight fixes the client id caching issue
expect(nfts).toMatchInlineSnapshot(`
[
{
@@ -49,7 +48,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFTs", () => {
"uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/0",
},
"owner": null,
"tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
"tokenAddress": "0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e",
"tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/0",
"type": "ERC721",
},
@@ -86,7 +85,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFTs", () => {
"uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1",
},
"owner": null,
"tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
"tokenAddress": "0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e",
"tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/1",
"type": "ERC721",
},
@@ -123,7 +122,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFTs", () => {
"uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/2",
},
"owner": null,
"tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
"tokenAddress": "0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e",
"tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/2",
"type": "ERC721",
},
@@ -160,7 +159,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFTs", () => {
"uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/3",
},
"owner": null,
"tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
"tokenAddress": "0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e",
"tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/3",
"type": "ERC721",
},
@@ -197,7 +196,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("erc721.getNFTs", () => {
"uri": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/4",
},
"owner": null,
"tokenAddress": "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
"tokenAddress": "0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e",
"tokenURI": "ipfs://QmPMc4tcBsMqLRuCQtPmPe84bpSjrC3Ky7t3JWuHXYB4aS/4",
"type": "ERC721",
},
@@ -188,7 +188,17 @@ function createAccount({
onTransactionRequested: async () => {
// make sure to show the coinbase popup BEFORE doing any transaction preprocessing
// otherwise the popup might get blocked in safari
await showCoinbasePopup(provider);
// but only if using cb smart wallet (web based)
if (window.localStorage) {
// this is the local storage key for the signer type in the cb web sdk
// value can be "scw" (web) or "walletlink" (mobile wallet)
const signerType = window.localStorage.getItem(
"-CBWSDK:SignerConfigurator:SignerType",
);
if (signerType === "scw") {
await showCoinbasePopup(provider);
}
}
},
async sendTransaction(tx: SendTransactionOption) {
const transactionHash = (await provider.request({
@@ -82,6 +82,7 @@ export function wrapFetchWithPayment(
parsedPaymentRequirements,
chain.id,
"exact",
error,
);
if (!selectedPaymentRequirements) {
@@ -141,10 +142,11 @@ function defaultPaymentRequirementsSelector(
paymentRequirements: RequestedPaymentRequirements[],
chainId: number,
scheme: "exact",
error?: string,
) {
if (!paymentRequirements.length) {
throw new Error(
"No valid payment requirements found in server 402 response",
`No valid payment requirements found in server 402 response. ${error}`,
);
}
const currentWalletNetwork = ChainIdToNetwork[chainId] || `eip155:${chainId}`;
+2
View File
@@ -1,5 +1,7 @@
# @thirdweb-dev/wagmi-adapter
## 0.2.164
## 0.2.163
## 0.2.162
+1 -1
View File
@@ -56,5 +56,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.163"
"version": "0.2.164"
}
+70 -115
View File
@@ -202,9 +202,6 @@ importers:
nuqs:
specifier: ^2.4.3
version: 2.4.3([email protected](@babel/[email protected])(@opentelemetry/[email protected])(@playwright/[email protected])([email protected]([email protected]))([email protected]))([email protected])
ogl:
specifier: ^1.0.11
version: 1.0.11
papaparse:
specifier: ^5.5.3
version: 5.5.3
@@ -581,7 +578,7 @@ importers:
dependencies:
'@abstract-foundation/agw-react':
specifier: ^1.6.4
version: 1.6.4(cp5zk5cv43kemeeoylvueylari)
version: 1.6.4(el4ps3mpgaztc4azwwa4wmobby)
'@ai-sdk/react':
specifier: ^2.0.25
version: 2.0.25([email protected])([email protected])
@@ -8426,6 +8423,7 @@ packages:
'@walletconnect/[email protected]':
resolution: {integrity: sha512-RQVt58oJ+rwqnPcIvRFeMGKuXb9qkgSmwz4noF8JZGUym3gUAzVs+uW2NQ1Owm9XOJAV+sANrtJ+VoVq1ftElw==}
deprecated: Please follow the migration guide on https://docs.reown.com/appkit/upgrade/wcm
'@walletconnect/[email protected]':
resolution: {integrity: sha512-lHKwXKoB0rdDH1ukxUx7o86xosWbttWIHYMZ8tgAQC1k9VH3CZZCoBcHOAAX8iBzyb0n0UP3/9zRrOcJE5nz7Q==}
@@ -13631,9 +13629,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==}
[email protected]:
resolution: {integrity: sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==}
[email protected]:
resolution: {integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==}
@@ -17301,11 +17296,11 @@ snapshots:
typescript: 5.8.3
optional: true
'@abstract-foundation/[email protected](cp5zk5cv43kemeeoylvueylari)':
'@abstract-foundation/[email protected](el4ps3mpgaztc4azwwa4wmobby)':
dependencies:
'@abstract-foundation/agw-client': 1.6.2([email protected]([email protected])([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))
'@privy-io/cross-app-connect': 0.2.2(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected])))([email protected]([email protected])([email protected])([email protected])([email protected]))
'@privy-io/react-auth': 2.17.3(@abstract-foundation/[email protected]([email protected]([email protected])([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])([email protected])))(@solana/[email protected](@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])([email protected])([email protected])([email protected]))(@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])
'@privy-io/react-auth': 2.17.3(@abstract-foundation/[email protected]([email protected]([email protected])([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])([email protected])))(@solana/[email protected](@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])([email protected])([email protected]))(@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])
'@tanstack/react-query': 5.81.5([email protected])
react: 19.1.0
secp256k1: 5.0.1
@@ -17431,10 +17426,10 @@ snapshots:
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.592.0
'@aws-sdk/client-sts': 3.592.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-host-header': 3.577.0
'@aws-sdk/middleware-logger': 3.577.0
'@aws-sdk/middleware-recursion-detection': 3.577.0
@@ -17477,10 +17472,10 @@ snapshots:
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.592.0
'@aws-sdk/client-sts': 3.592.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-host-header': 3.577.0
'@aws-sdk/middleware-logger': 3.577.0
'@aws-sdk/middleware-recursion-detection': 3.577.0
@@ -17523,10 +17518,10 @@ snapshots:
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.592.0
'@aws-sdk/client-sts': 3.592.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-host-header': 3.577.0
'@aws-sdk/middleware-logger': 3.577.0
'@aws-sdk/middleware-recursion-detection': 3.577.0
@@ -17570,7 +17565,7 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
'@aws-sdk/[email protected]':
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
@@ -17613,7 +17608,6 @@ snapshots:
'@smithy/util-utf8': 3.0.0
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sts'
- aws-crt
'@aws-sdk/[email protected]':
@@ -17750,7 +17744,7 @@ snapshots:
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.592.0
'@aws-sdk/core': 3.592.0
'@aws-sdk/credential-provider-node': 3.592.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-host-header': 3.577.0
@@ -17869,24 +17863,6 @@ snapshots:
'@smithy/util-stream': 4.2.3
tslib: 2.8.1
'@aws-sdk/[email protected](@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/client-sts': 3.592.0
'@aws-sdk/credential-provider-env': 3.587.0
'@aws-sdk/credential-provider-http': 3.587.0
'@aws-sdk/credential-provider-process': 3.587.0
'@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/[email protected])
'@aws-sdk/types': 3.577.0
'@smithy/credential-provider-imds': 3.2.8
'@smithy/property-provider': 3.1.11
'@smithy/shared-ini-file-loader': 3.1.12
'@smithy/types': 3.7.2
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sso-oidc'
- aws-crt
'@aws-sdk/[email protected](@aws-sdk/[email protected])(@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/client-sts': 3.592.0
@@ -17941,25 +17917,6 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/[email protected](@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/credential-provider-env': 3.587.0
'@aws-sdk/credential-provider-http': 3.587.0
'@aws-sdk/credential-provider-ini': 3.592.0(@aws-sdk/[email protected](@aws-sdk/[email protected]))(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-process': 3.587.0
'@aws-sdk/credential-provider-sso': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-web-identity': 3.587.0(@aws-sdk/[email protected])
'@aws-sdk/types': 3.577.0
'@smithy/credential-provider-imds': 3.2.8
'@smithy/property-provider': 3.1.11
'@smithy/shared-ini-file-loader': 3.1.12
'@smithy/types': 3.7.2
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sso-oidc'
- '@aws-sdk/client-sts'
- aws-crt
'@aws-sdk/[email protected](@aws-sdk/[email protected])(@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/credential-provider-env': 3.587.0
@@ -18233,7 +18190,7 @@ snapshots:
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/client-sso-oidc': 3.592.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.592.0
'@aws-sdk/types': 3.577.0
'@smithy/property-provider': 3.1.11
'@smithy/shared-ini-file-loader': 3.1.12
@@ -22973,7 +22930,7 @@ snapshots:
- typescript
- utf-8-validate
'@privy-io/[email protected](@abstract-foundation/[email protected]([email protected]([email protected])([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])([email protected])))(@solana/[email protected](@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])([email protected])([email protected])([email protected]))(@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])':
'@privy-io/[email protected](@abstract-foundation/[email protected]([email protected]([email protected])([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])([email protected])))(@solana/[email protected](@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])([email protected])([email protected]))(@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected])':
dependencies:
'@coinbase/wallet-sdk': 4.3.2
'@floating-ui/react': 0.26.28([email protected]([email protected]))([email protected])
@@ -23020,7 +22977,7 @@ snapshots:
zustand: 5.0.7(@types/[email protected])([email protected])([email protected]([email protected]))
optionalDependencies:
'@abstract-foundation/agw-client': 1.8.5([email protected]([email protected])([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))
'@solana/spl-token': 0.4.14(@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])([email protected])([email protected])([email protected])
'@solana/spl-token': 0.4.14(@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])([email protected])([email protected])
'@solana/web3.js': 1.98.2([email protected])([email protected])([email protected])([email protected])
transitivePeerDependencies:
- '@azure/app-configuration'
@@ -26524,12 +26481,11 @@ snapshots:
'@solana/errors': 4.0.0([email protected])
typescript: 5.8.3
'@solana/[email protected]([email protected])([email protected])':
'@solana/[email protected]([email protected])':
dependencies:
'@solana/codecs-core': 2.0.0-rc.1([email protected])
'@solana/codecs-numbers': 2.0.0-rc.1([email protected])
'@solana/errors': 2.0.0-rc.1([email protected])
fastestsmallesttextencoderdecoder: 1.0.22
typescript: 5.8.3
optional: true
@@ -26557,13 +26513,13 @@ snapshots:
fastestsmallesttextencoderdecoder: 1.0.22
typescript: 5.8.3
'@solana/[email protected]([email protected])([email protected])':
'@solana/[email protected]([email protected])':
dependencies:
'@solana/codecs-core': 2.0.0-rc.1([email protected])
'@solana/codecs-data-structures': 2.0.0-rc.1([email protected])
'@solana/codecs-numbers': 2.0.0-rc.1([email protected])
'@solana/codecs-strings': 2.0.0-rc.1([email protected])([email protected])
'@solana/options': 2.0.0-rc.1([email protected])([email protected])
'@solana/codecs-strings': 2.0.0-rc.1([email protected])
'@solana/options': 2.0.0-rc.1([email protected])
typescript: 5.8.3
transitivePeerDependencies:
- fastestsmallesttextencoderdecoder
@@ -26765,12 +26721,12 @@ snapshots:
dependencies:
typescript: 5.8.3
'@solana/[email protected]([email protected])([email protected])':
'@solana/[email protected]([email protected])':
dependencies:
'@solana/codecs-core': 2.0.0-rc.1([email protected])
'@solana/codecs-data-structures': 2.0.0-rc.1([email protected])
'@solana/codecs-numbers': 2.0.0-rc.1([email protected])
'@solana/codecs-strings': 2.0.0-rc.1([email protected])([email protected])
'@solana/codecs-strings': 2.0.0-rc.1([email protected])
'@solana/errors': 2.0.0-rc.1([email protected])
typescript: 5.8.3
transitivePeerDependencies:
@@ -27178,30 +27134,30 @@ snapshots:
transitivePeerDependencies:
- fastestsmallesttextencoderdecoder
'@solana/[email protected](@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])':
'@solana/[email protected](@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])':
dependencies:
'@solana/codecs': 2.0.0-rc.1([email protected])([email protected])
'@solana/codecs': 2.0.0-rc.1([email protected])
'@solana/web3.js': 1.98.2([email protected])([email protected])([email protected])([email protected])
transitivePeerDependencies:
- fastestsmallesttextencoderdecoder
- typescript
optional: true
'@solana/[email protected](@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])':
'@solana/[email protected](@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])':
dependencies:
'@solana/codecs': 2.0.0-rc.1([email protected])([email protected])
'@solana/codecs': 2.0.0-rc.1([email protected])
'@solana/web3.js': 1.98.2([email protected])([email protected])([email protected])([email protected])
transitivePeerDependencies:
- fastestsmallesttextencoderdecoder
- typescript
optional: true
'@solana/[email protected](@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])([email protected])([email protected])([email protected])':
'@solana/[email protected](@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])([email protected])([email protected])':
dependencies:
'@solana/buffer-layout': 4.0.1
'@solana/buffer-layout-utils': 0.2.0([email protected])([email protected])([email protected])([email protected])
'@solana/spl-token-group': 0.0.7(@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])
'@solana/spl-token-metadata': 0.1.6(@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])([email protected])
'@solana/spl-token-group': 0.0.7(@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])
'@solana/spl-token-metadata': 0.1.6(@solana/[email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])
'@solana/web3.js': 1.98.2([email protected])([email protected])([email protected])([email protected])
buffer: 6.0.3
transitivePeerDependencies:
@@ -29213,19 +29169,19 @@ snapshots:
- bufferutil
- utf-8-validate
'@wagmi/[email protected](@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@4.1.11)))([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))([email protected]))([email protected])':
'@wagmi/[email protected](@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@3.25.75)))([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))([email protected]))([email protected])':
dependencies:
'@base-org/account': 1.1.1(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])(zod@4.1.11)
'@coinbase/wallet-sdk': 4.3.6(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])(zod@4.1.11)
'@gemini-wallet/core': 0.2.0([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))
'@base-org/account': 1.1.1(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])(zod@3.25.75)
'@coinbase/wallet-sdk': 4.3.6(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])(zod@3.25.75)
'@gemini-wallet/core': 0.2.0([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))
'@metamask/sdk': 0.33.1([email protected])([email protected])([email protected])
'@safe-global/safe-apps-provider': 0.18.6([email protected])([email protected])([email protected])(zod@4.1.11)
'@safe-global/safe-apps-sdk': 9.1.0([email protected])([email protected])([email protected])(zod@4.1.11)
'@wagmi/core': 2.21.2(@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))
'@walletconnect/ethereum-provider': 2.21.1(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])(zod@4.1.11)
'@safe-global/safe-apps-provider': 0.18.6([email protected])([email protected])([email protected])(zod@3.25.75)
'@safe-global/safe-apps-sdk': 9.1.0([email protected])([email protected])([email protected])(zod@3.25.75)
'@wagmi/core': 2.21.2(@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))
'@walletconnect/ethereum-provider': 2.21.1(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])(zod@3.25.75)
cbw-sdk: '@coinbase/[email protected]'
porto: 0.2.19(@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@4.1.11)))([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))([email protected]))
viem: 2.37.9([email protected])([email protected])([email protected])(zod@4.1.11)
porto: 0.2.19(@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@3.25.75)))([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))([email protected]))
viem: 2.37.9([email protected])([email protected])([email protected])(zod@3.25.75)
optionalDependencies:
typescript: 5.8.3
transitivePeerDependencies:
@@ -29259,19 +29215,19 @@ snapshots:
- wagmi
- zod
'@wagmi/[email protected](@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])(use-sync-external-store@1.5.0([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@3.25.75)))([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))([email protected]))([email protected])':
'@wagmi/[email protected](@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])(use-sync-external-store@1.4.0([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@4.1.11)))([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))([email protected]))([email protected])':
dependencies:
'@base-org/account': 1.1.1(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])(zod@3.25.75)
'@coinbase/wallet-sdk': 4.3.6(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])(zod@3.25.75)
'@gemini-wallet/core': 0.2.0([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))
'@base-org/account': 1.1.1(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])(zod@4.1.11)
'@coinbase/wallet-sdk': 4.3.6(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])(zod@4.1.11)
'@gemini-wallet/core': 0.2.0([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))
'@metamask/sdk': 0.33.1([email protected])([email protected])([email protected])
'@safe-global/safe-apps-provider': 0.18.6([email protected])([email protected])([email protected])(zod@3.25.75)
'@safe-global/safe-apps-sdk': 9.1.0([email protected])([email protected])([email protected])(zod@3.25.75)
'@wagmi/core': 2.21.2(@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])(use-sync-external-store@1.5.0([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))
'@walletconnect/ethereum-provider': 2.21.1(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])(zod@3.25.75)
'@safe-global/safe-apps-provider': 0.18.6([email protected])([email protected])([email protected])(zod@4.1.11)
'@safe-global/safe-apps-sdk': 9.1.0([email protected])([email protected])([email protected])(zod@4.1.11)
'@wagmi/core': 2.21.2(@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])(use-sync-external-store@1.4.0([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))
'@walletconnect/ethereum-provider': 2.21.1(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])(zod@4.1.11)
cbw-sdk: '@coinbase/[email protected]'
porto: 0.2.19(@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])(use-sync-external-store@1.5.0([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@3.25.75)))([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])(zod@3.25.75))([email protected]))
viem: 2.37.9([email protected])([email protected])([email protected])(zod@3.25.75)
porto: 0.2.19(@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])(use-sync-external-store@1.4.0([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@4.1.11)))([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])(zod@4.1.11))([email protected]))
viem: 2.37.9([email protected])([email protected])([email protected])(zod@4.1.11)
optionalDependencies:
typescript: 5.8.3
transitivePeerDependencies:
@@ -29403,6 +29359,7 @@ snapshots:
- immer
- react
- use-sync-external-store
optional: true
'@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected]))':
dependencies:
@@ -37603,8 +37560,6 @@ snapshots:
node-fetch-native: 1.6.6
ufo: 1.6.1
[email protected]: {}
[email protected]: {}
[email protected]: {}
@@ -38273,6 +38228,26 @@ snapshots:
[email protected]: {}
[email protected](@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected])))([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])):
dependencies:
'@wagmi/core': 2.21.2(@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected]))
hono: 4.9.9
idb-keyval: 6.2.2
mipd: 0.0.7([email protected])
ox: 0.9.7([email protected])([email protected])
viem: 2.37.9([email protected])([email protected])([email protected])([email protected])
zod: 4.1.11
zustand: 5.0.7(@types/[email protected])([email protected])([email protected]([email protected]))
optionalDependencies:
'@tanstack/react-query': 5.81.5([email protected])
react: 19.1.0
typescript: 5.8.3
wagmi: 2.17.5(@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])
transitivePeerDependencies:
- '@types/react'
- immer
- use-sync-external-store
[email protected](@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected])))([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])):
dependencies:
'@wagmi/core': 2.21.2(@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected]))
@@ -38293,26 +38268,6 @@ snapshots:
- immer
- use-sync-external-store
[email protected](@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected])))([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])):
dependencies:
'@wagmi/core': 2.21.2(@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected]))
hono: 4.9.9
idb-keyval: 6.2.2
mipd: 0.0.7([email protected])
ox: 0.9.7([email protected])([email protected])
viem: 2.37.9([email protected])([email protected])([email protected])([email protected])
zod: 4.1.11
zustand: 5.0.7(@types/[email protected])([email protected])([email protected]([email protected]))
optionalDependencies:
'@tanstack/react-query': 5.81.5([email protected])
react: 19.1.0
typescript: 5.8.3
wagmi: 2.17.5(@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected])
transitivePeerDependencies:
- '@types/react'
- immer
- use-sync-external-store
[email protected]: {}
[email protected]([email protected]):
@@ -41726,7 +41681,7 @@ snapshots:
[email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected]):
dependencies:
'@tanstack/react-query': 5.81.5([email protected])
'@wagmi/connectors': 5.11.2(@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])(use-sync-external-store@1.5.0([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected])))([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected]))([email protected])
'@wagmi/connectors': 5.11.2(@tanstack/[email protected]([email protected]))(@types/[email protected])(@wagmi/[email protected](@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])(use-sync-external-store@1.4.0([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected])))([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected](@tanstack/[email protected])(@tanstack/[email protected]([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected])([email protected]([email protected])([email protected])([email protected])([email protected]))([email protected]))([email protected])
'@wagmi/core': 2.21.2(@tanstack/[email protected])(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected])([email protected])([email protected])([email protected]))
react: 19.1.0
use-sync-external-store: 1.4.0([email protected])