Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47354d5a19 | ||
|
|
8280efb991 | ||
|
|
5f11ae0cd4 | ||
|
|
c9a0c23c6c | ||
|
|
43b0680124 | ||
|
|
cccbde9a2e | ||
|
|
85a6afc377 | ||
|
|
e74aca9c90 | ||
|
|
b0cac940bd | ||
|
|
1a19e14845 | ||
|
|
464af6a54c |
@@ -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"
|
||||
>
|
||||
|
||||
+12
-1
@@ -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,
|
||||
|
||||
+120
@@ -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,
|
||||
},
|
||||
};
|
||||
+404
@@ -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;
|
||||
}
|
||||
}
|
||||
+349
-126
@@ -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>
|
||||
);
|
||||
}
|
||||
+36
-20
@@ -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>
|
||||
);
|
||||
|
||||
-84
@@ -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>
|
||||
);
|
||||
}
|
||||
+12
-7
@@ -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>
|
||||
);
|
||||
|
||||
+72
@@ -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>
|
||||
);
|
||||
}
|
||||
+1
@@ -156,6 +156,7 @@ export async function PayAnalytics(props: {
|
||||
client={props.client}
|
||||
projectClientId={props.projectClientId}
|
||||
teamId={props.teamId}
|
||||
authToken={props.authToken}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+2
@@ -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;
|
||||
},
|
||||
|
||||
+2
@@ -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"
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
+9
@@ -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;
|
||||
},
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
+392
-196
@@ -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>
|
||||
|
||||
+2
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
+2
@@ -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>
|
||||
);
|
||||
|
||||
+138
@@ -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: [
|
||||
{
|
||||
|
||||
+283
@@ -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");
|
||||
},
|
||||
},
|
||||
};
|
||||
+83
@@ -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>
|
||||
);
|
||||
}
|
||||
+490
@@ -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>
|
||||
);
|
||||
}
|
||||
+3
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "wagmi-inapp",
|
||||
"private": true,
|
||||
"version": "0.0.8",
|
||||
"version": "0.0.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.164
|
||||
|
||||
## 0.2.163
|
||||
|
||||
## 0.2.162
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+70
-115
Reference in New Issue
Block a user