Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a82e63319a | ||
|
|
23d74a0a54 | ||
|
|
eb07340118 | ||
|
|
dc07145543 | ||
|
|
901789983f | ||
|
|
ad49560331 | ||
|
|
83198b2ea4 |
@@ -2,7 +2,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { rotateVaultAccountAndAccessToken } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client";
|
||||
import { accountKeys, authorizedWallets } from "../query-keys/cache-keys";
|
||||
|
||||
// FIXME: We keep repeating types, API server should provide them
|
||||
@@ -316,6 +315,20 @@ export type RotateSecretKeyAPIReturnType = {
|
||||
};
|
||||
};
|
||||
|
||||
export const MANAGED_VAULT_BLOCKS_ROTATION_CODE =
|
||||
"MANAGED_VAULT_BLOCKS_ROTATION";
|
||||
|
||||
export class RotateSecretKeyError extends Error {
|
||||
code: string | undefined;
|
||||
status: number;
|
||||
constructor(message: string, status: number, code?: string) {
|
||||
super(message);
|
||||
this.name = "RotateSecretKeyError";
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export async function rotateSecretKeyClient(params: { project: Project }) {
|
||||
const res = await apiServerProxy<RotateSecretKeyAPIReturnType>({
|
||||
body: JSON.stringify({}),
|
||||
@@ -327,19 +340,24 @@ export async function rotateSecretKeyClient(params: { project: Project }) {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
// if the project has an encrypted vault admin key, rotate it as well
|
||||
const service = params.project.services.find(
|
||||
(service) => service.name === "engineCloud",
|
||||
);
|
||||
if (service?.encryptedAdminKey) {
|
||||
await rotateVaultAccountAndAccessToken({
|
||||
project: params.project,
|
||||
projectSecretKey: res.data.data.secret,
|
||||
projectSecretHash: res.data.data.secretHash,
|
||||
});
|
||||
// The error body is a JSON-serialized `{ error: { code, message, ... } }`
|
||||
// payload from the api-server. Try to extract the structured code so the
|
||||
// UI can react (e.g. redirect users with a managed vault to the vault
|
||||
// configuration page so they can eject first).
|
||||
let code: string | undefined;
|
||||
let message = res.error;
|
||||
try {
|
||||
const parsed = JSON.parse(res.error) as {
|
||||
error?: { code?: string; message?: string };
|
||||
};
|
||||
code = parsed.error?.code;
|
||||
if (parsed.error?.message) {
|
||||
message = parsed.error.message;
|
||||
}
|
||||
} catch {
|
||||
// not JSON, fall through with raw text
|
||||
}
|
||||
throw new RotateSecretKeyError(message, res.status, code);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
|
||||
@@ -19,25 +19,30 @@ export const TEAM_PLANS: Record<
|
||||
}
|
||||
> = {
|
||||
growth: {
|
||||
description: "Ideal for teams building production-grade apps.",
|
||||
description: "Ideal for small scale teams and startups.",
|
||||
features: [
|
||||
"Email Support",
|
||||
"48hr Guaranteed Response",
|
||||
"Custom User Wallet Auth",
|
||||
"Custom In-App Wallet Auth",
|
||||
"Web, Mobile & Gaming SDKs",
|
||||
"Server Wallets",
|
||||
"Contract & Wallet APIs",
|
||||
"Account Abstraction",
|
||||
"Managed Infrastructure (RPC, IPFS, etc.)",
|
||||
"Audited smart contracts",
|
||||
],
|
||||
price: 99,
|
||||
subTitle: "Everything in Starter, plus:",
|
||||
subTitle: null,
|
||||
title: "Growth",
|
||||
trialPeriodDays: 0,
|
||||
},
|
||||
pro: {
|
||||
description: "For large organizations with custom needs.",
|
||||
description: "For large orgs with custom needs.",
|
||||
features: [
|
||||
"Dedicated Account Executive",
|
||||
"Slack & Telegram Support",
|
||||
"12hr Guaranteed Response",
|
||||
"No Rate Limits",
|
||||
["Custom Infrastructure Add-Ons", "Infrastructure for custom chains."],
|
||||
["Volume Discounts", "Negotiated volume discounts that fit your scale."],
|
||||
"Custom Infrastructure Add-Ons",
|
||||
"Negotiated Volume Discounts",
|
||||
],
|
||||
isStartingPriceOnly: true,
|
||||
price: 1499,
|
||||
@@ -46,20 +51,14 @@ export const TEAM_PLANS: Record<
|
||||
trialPeriodDays: 0,
|
||||
},
|
||||
scale: {
|
||||
description: "For funded startups and mid-size businesses.",
|
||||
description: "Ideal for mid-size businesses.",
|
||||
features: [
|
||||
"Slack & Telegram Support",
|
||||
"Email Support",
|
||||
"24hr Guaranteed Response",
|
||||
"Remove thirdweb branding",
|
||||
"Audit Logs",
|
||||
[
|
||||
"Ecosystem Wallet Add-On",
|
||||
"Unlocks the ability to deploy your own ecosystem wallets.",
|
||||
],
|
||||
[
|
||||
"Dedicated Infrastructure Add-Ons",
|
||||
"Dedicated RPC nodes, indexers, etc.",
|
||||
],
|
||||
"Ecosystem Wallet Add-On",
|
||||
"Dedicated Infrastructure Add-Ons (RPC, Insight)",
|
||||
],
|
||||
price: 499,
|
||||
subTitle: "Everything in Growth, plus:",
|
||||
|
||||
+1
-4
@@ -177,10 +177,7 @@ export function PlanInfoCardUI(props: {
|
||||
<div className="flex flex-col items-center py-8 text-center max-sm:gap-4">
|
||||
<CircleAlertIcon className="mb-3 text-muted-foreground lg:size-6" />
|
||||
<p>Your plan includes a fixed amount of free usage.</p>
|
||||
<p>
|
||||
To unlock additional usage, upgrade your plan to Starter or
|
||||
Growth.
|
||||
</p>
|
||||
<p>To unlock additional usage, upgrade your plan to Growth.</p>
|
||||
|
||||
<div className="mt-4">
|
||||
<ToolTipLabel
|
||||
|
||||
+1
-18
@@ -69,7 +69,6 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
validTeamPlan === "starter" ||
|
||||
validTeamPlan === "growth_legacy"));
|
||||
|
||||
const highlightStarterPlan = highlightPlan === "starter";
|
||||
const highlightScalePlan =
|
||||
highlightPlan === "scale" ||
|
||||
(!highlightPlan &&
|
||||
@@ -94,23 +93,7 @@ export const BillingPricing: React.FC<BillingPricingProps> = ({
|
||||
|
||||
<div className="h-5" />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
{/* Starter */}
|
||||
<PricingCard
|
||||
billingPlan="starter"
|
||||
billingStatus={team.billingStatus}
|
||||
cta={getPlanCta(
|
||||
validTeamPlan,
|
||||
"starter",
|
||||
isCurrentPlanScheduledToCancel,
|
||||
)}
|
||||
current={validTeamPlan === "starter"}
|
||||
getTeam={getTeam}
|
||||
highlighted={highlightStarterPlan}
|
||||
teamId={team.id}
|
||||
teamSlug={team.slug}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{/* Growth */}
|
||||
<PricingCard
|
||||
activeTrialEndsAt={
|
||||
|
||||
+19
-21
@@ -31,8 +31,7 @@ export function TeamDedicatedSupportCard(props: {
|
||||
const [selectedChannelType, setSelectedChannelType] =
|
||||
useState<ChannelType>("slack");
|
||||
|
||||
const isFeatureEnabled =
|
||||
props.team.billingPlan === "scale" || props.team.billingPlan === "pro";
|
||||
const isFeatureEnabled = props.team.billingPlan === "pro";
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (params: {
|
||||
@@ -107,8 +106,7 @@ export function TeamDedicatedSupportCard(props: {
|
||||
bottomText={
|
||||
!isFeatureEnabled ? (
|
||||
<>
|
||||
Upgrade to the <b>Scale</b> or <b>Pro</b> plan to unlock this
|
||||
feature.
|
||||
Upgrade to the <b>Pro</b> plan to unlock this feature.
|
||||
</>
|
||||
) : hasDefaultTeamName ? (
|
||||
"Please update your team name before requesting a dedicated support channel."
|
||||
@@ -126,17 +124,7 @@ export function TeamDedicatedSupportCard(props: {
|
||||
}
|
||||
saveButton={
|
||||
isFeatureEnabled
|
||||
? {
|
||||
disabled: createMutation.isPending,
|
||||
isPending: createMutation.isPending,
|
||||
label: "Create Support Channel",
|
||||
onClick: () =>
|
||||
createMutation.mutate({
|
||||
channelType: selectedChannelType,
|
||||
teamId: props.team.id,
|
||||
}),
|
||||
}
|
||||
: hasDefaultTeamName
|
||||
? hasDefaultTeamName
|
||||
? {
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
@@ -145,14 +133,24 @@ export function TeamDedicatedSupportCard(props: {
|
||||
router.push(`/team/${props.team.slug}/~/settings`),
|
||||
}
|
||||
: {
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
label: "Upgrade Plan",
|
||||
disabled: createMutation.isPending,
|
||||
isPending: createMutation.isPending,
|
||||
label: "Create Support Channel",
|
||||
onClick: () =>
|
||||
router.push(
|
||||
`/team/${props.team.slug}/~/billing?showPlans=true&highlight=scale`,
|
||||
),
|
||||
createMutation.mutate({
|
||||
channelType: selectedChannelType,
|
||||
teamId: props.team.id,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
label: "Upgrade Plan",
|
||||
onClick: () =>
|
||||
router.push(
|
||||
`/team/${props.team.slug}/~/billing?showPlans=true&highlight=pro`,
|
||||
),
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="md:w-[450px]">
|
||||
|
||||
+12
-2
@@ -32,7 +32,10 @@ export function ProjectFTUX(props: {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-14">
|
||||
<IntegrateAPIKeySection project={props.project} />
|
||||
<IntegrateAPIKeySection
|
||||
project={props.project}
|
||||
teamSlug={props.teamSlug}
|
||||
/>
|
||||
{props.projectWalletSection}
|
||||
<GetStartedSection project={props.project} />
|
||||
<ProductsSection
|
||||
@@ -47,7 +50,13 @@ export function ProjectFTUX(props: {
|
||||
|
||||
// Integrate API key section ------------------------------------------------------------
|
||||
|
||||
function IntegrateAPIKeySection({ project }: { project: Project }) {
|
||||
function IntegrateAPIKeySection({
|
||||
project,
|
||||
teamSlug,
|
||||
}: {
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
}) {
|
||||
const secretKeyMasked = project.secretKeys[0]?.masked;
|
||||
const clientId = project.publishableKey;
|
||||
|
||||
@@ -81,6 +90,7 @@ function IntegrateAPIKeySection({ project }: { project: Project }) {
|
||||
<SecretKeySection
|
||||
project={project}
|
||||
secretKeyMasked={secretKeyMasked}
|
||||
vaultConfigUrl={`/team/${teamSlug}/${project.slug}/wallets/server-wallets/configuration`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+2
@@ -8,6 +8,7 @@ import { RotateSecretKeyButton } from "../../settings/ProjectGeneralSettingsPage
|
||||
export function SecretKeySection(props: {
|
||||
secretKeyMasked: string;
|
||||
project: Project;
|
||||
vaultConfigUrl: string;
|
||||
}) {
|
||||
const [secretKeyMasked, setSecretKeyMasked] = useState(props.secretKeyMasked);
|
||||
|
||||
@@ -34,6 +35,7 @@ export function SecretKeySection(props: {
|
||||
project: props.project,
|
||||
});
|
||||
}}
|
||||
vaultConfigUrl={props.vaultConfigUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+29
-1
@@ -62,6 +62,8 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import type { RotateSecretKeyAPIReturnType } from "@/hooks/useApi";
|
||||
import {
|
||||
deleteProjectClient,
|
||||
MANAGED_VAULT_BLOCKS_ROTATION_CODE,
|
||||
RotateSecretKeyError,
|
||||
rotateSecretKeyClient,
|
||||
updateProjectClient,
|
||||
} from "@/hooks/useApi";
|
||||
@@ -100,6 +102,7 @@ type ProjectSettingPaths = {
|
||||
inAppConfig: string;
|
||||
aaConfig: string;
|
||||
payConfig: string;
|
||||
vaultConfig: string;
|
||||
afterDeleteRedirectTo: string;
|
||||
};
|
||||
|
||||
@@ -224,6 +227,7 @@ export function ProjectGeneralSettingsPageUI(props: {
|
||||
afterDeleteRedirectTo: `/team/${props.teamSlug}`,
|
||||
inAppConfig: `${projectLayout}/wallets/user-wallets/configuration`,
|
||||
payConfig: `${projectLayout}/bridge/configuration`,
|
||||
vaultConfig: `${projectLayout}/wallets/server-wallets/configuration`,
|
||||
};
|
||||
|
||||
const { project } = props;
|
||||
@@ -343,6 +347,7 @@ export function ProjectGeneralSettingsPageUI(props: {
|
||||
<ProjectKeyDetails
|
||||
project={project}
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
vaultConfigUrl={paths.vaultConfig}
|
||||
/>
|
||||
<ProjectIdCard project={project} />
|
||||
<AllowedDomainsSetting
|
||||
@@ -845,9 +850,11 @@ function EnabledServicesSetting(props: {
|
||||
function ProjectKeyDetails({
|
||||
project,
|
||||
rotateSecretKey,
|
||||
vaultConfigUrl,
|
||||
}: {
|
||||
rotateSecretKey: RotateSecretKey;
|
||||
project: Project;
|
||||
vaultConfigUrl: string;
|
||||
}) {
|
||||
// currently only showing the first secret key
|
||||
const { createdAt, updatedAt, lastAccessedAt } = project;
|
||||
@@ -893,6 +900,7 @@ function ProjectKeyDetails({
|
||||
setSecretKeyMasked(data.data.secretMasked);
|
||||
}}
|
||||
rotateSecretKey={rotateSecretKey}
|
||||
vaultConfigUrl={vaultConfigUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -975,6 +983,7 @@ function DeleteProject(props: {
|
||||
export function RotateSecretKeyButton(props: {
|
||||
rotateSecretKey: RotateSecretKey;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
vaultConfigUrl: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isModalCloseAllowed, setIsModalCloseAllowed] = useState(true);
|
||||
@@ -1011,6 +1020,7 @@ export function RotateSecretKeyButton(props: {
|
||||
disableModalClose={() => setIsModalCloseAllowed(false)}
|
||||
onSuccess={props.onSuccess}
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
vaultConfigUrl={props.vaultConfigUrl}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -1026,6 +1036,7 @@ function RotateSecretKeyModalContent(props: {
|
||||
closeModal: () => void;
|
||||
disableModalClose: () => void;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
vaultConfigUrl: string;
|
||||
}) {
|
||||
const [screen, setScreen] = useState<RotateSecretKeyScreen>({
|
||||
id: "initial",
|
||||
@@ -1050,6 +1061,7 @@ function RotateSecretKeyModalContent(props: {
|
||||
setScreen({ id: "save-newkey", secretKey: data.data.secret });
|
||||
}}
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
vaultConfigUrl={props.vaultConfigUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1061,13 +1073,29 @@ function RotateSecretKeyInitialScreen(props: {
|
||||
rotateSecretKey: RotateSecretKey;
|
||||
onSuccess: (data: RotateSecretKeyAPIReturnType) => void;
|
||||
closeModal: () => void;
|
||||
vaultConfigUrl: string;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const [isConfirmed, setIsConfirmed] = useState(false);
|
||||
const rotateKeyMutation = useMutation({
|
||||
mutationFn: props.rotateSecretKey,
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
toast.error("Failed to rotate secret key");
|
||||
if (
|
||||
err instanceof RotateSecretKeyError &&
|
||||
err.code === MANAGED_VAULT_BLOCKS_ROTATION_CODE
|
||||
) {
|
||||
toast.error("Eject your server-wallet vault first", {
|
||||
description:
|
||||
"This project has a managed vault. Redirecting you to the vault configuration page so you can eject it before rotating the secret key.",
|
||||
});
|
||||
props.closeModal();
|
||||
router.push(props.vaultConfigUrl);
|
||||
return;
|
||||
}
|
||||
toast.error("Failed to rotate secret key", {
|
||||
description: err instanceof Error ? err.message : undefined,
|
||||
});
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
props.onSuccess(data);
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# wagmi-inapp
|
||||
|
||||
## 0.0.57
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`23d74a0`](https://github.com/thirdweb-dev/js/commit/23d74a0a54247930e6889bbf66f88fa673a23f1a)]:
|
||||
- thirdweb@5.120.0
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.212
|
||||
|
||||
## 0.0.56
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`83198b2`](https://github.com/thirdweb-dev/js/commit/83198b2ea4dd29b1debb7db2dc2eabba57e9c4d7), [`ad49560`](https://github.com/thirdweb-dev/js/commit/ad4956033107c52ecdf4cd9f5b2bff87867f3bec)]:
|
||||
- thirdweb@5.119.4
|
||||
- @thirdweb-dev/wagmi-adapter@0.2.211
|
||||
|
||||
## 0.0.55
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wagmi-inapp",
|
||||
"private": true,
|
||||
"version": "0.0.55",
|
||||
"version": "0.0.57",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @thirdweb-dev/nebula
|
||||
|
||||
## 0.2.115
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`23d74a0`](https://github.com/thirdweb-dev/js/commit/23d74a0a54247930e6889bbf66f88fa673a23f1a)]:
|
||||
- thirdweb@5.120.0
|
||||
|
||||
## 0.2.114
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`83198b2`](https://github.com/thirdweb-dev/js/commit/83198b2ea4dd29b1debb7db2dc2eabba57e9c4d7), [`ad49560`](https://github.com/thirdweb-dev/js/commit/ad4956033107c52ecdf4cd9f5b2bff87867f3bec)]:
|
||||
- thirdweb@5.119.4
|
||||
|
||||
## 0.2.113
|
||||
|
||||
### 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.113"
|
||||
"version": "0.2.115"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# thirdweb
|
||||
|
||||
## 5.120.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#8764](https://github.com/thirdweb-dev/js/pull/8764) [`23d74a0`](https://github.com/thirdweb-dev/js/commit/23d74a0a54247930e6889bbf66f88fa673a23f1a) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - [SDK] Add RampNow as a new onramp provider
|
||||
|
||||
## 5.119.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#8733](https://github.com/thirdweb-dev/js/pull/8733) [`83198b2`](https://github.com/thirdweb-dev/js/commit/83198b2ea4dd29b1debb7db2dc2eabba57e9c4d7) Thanks [@Dhaiwat10](https://github.com/Dhaiwat10)! - Updates the hardcoded ENS Universal Resolver address from 0xce01f8eee7E479C928F8919abD53E553a36CeF67 (old) to 0xeeeeeeee14d718c2b47d9923deab1335e144eeee (new)
|
||||
|
||||
- [#8735](https://github.com/thirdweb-dev/js/pull/8735) [`ad49560`](https://github.com/thirdweb-dev/js/commit/ad4956033107c52ecdf4cd9f5b2bff87867f3bec) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - ENS: add coinType param to reverse resolver
|
||||
|
||||
## 5.119.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -431,5 +431,5 @@
|
||||
}
|
||||
},
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "5.119.3"
|
||||
"version": "5.120.0"
|
||||
}
|
||||
|
||||
@@ -115,4 +115,33 @@ describe.runIf(process.env.TW_SECRET_KEY)("Bridge.Onramp.prepare", () => {
|
||||
// Steps array should be defined (it may be empty if the provider supports the destination token natively)
|
||||
expect(Array.isArray(prepared.steps)).toBe(true);
|
||||
});
|
||||
|
||||
// The Rampnow live-API test is gated on a separate env var because the
|
||||
// server-side schema for `onramp: "rampnow"` lands in a separate deploy.
|
||||
// Once `api.thirdweb-dev.com` accepts the new provider, drop the runIf and
|
||||
// mirror the stripe/coinbase/transak assertions above.
|
||||
it.runIf(process.env.TW_BRIDGE_RAMPNOW)(
|
||||
"should prepare a Rampnow onramp successfully",
|
||||
async () => {
|
||||
const prepared = await Onramp.prepare({
|
||||
amount: toWei("0.01"),
|
||||
chainId: 1,
|
||||
client: TEST_CLIENT,
|
||||
onramp: "rampnow",
|
||||
receiver: RECEIVER_ADDRESS,
|
||||
tokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
});
|
||||
|
||||
expect(prepared).toBeDefined();
|
||||
expect(typeof prepared.destinationAmount).toBe("bigint");
|
||||
expect(prepared.destinationAmount > 0n).toBe(true);
|
||||
expect(prepared.link).toBeDefined();
|
||||
expect(typeof prepared.link).toBe("string");
|
||||
expect(prepared.intent).toBeDefined();
|
||||
expect(prepared.intent.receiver.toLowerCase()).toBe(
|
||||
RECEIVER_ADDRESS.toLowerCase(),
|
||||
);
|
||||
expect(Array.isArray(prepared.steps)).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { TokenWithPrices } from "./types/Token.js";
|
||||
export { status } from "./OnrampStatus.js";
|
||||
|
||||
type OnrampIntent = {
|
||||
onramp: "stripe" | "coinbase" | "transak";
|
||||
onramp: "stripe" | "coinbase" | "transak" | "rampnow";
|
||||
chainId: number;
|
||||
tokenAddress: ox__Address.Address;
|
||||
receiver: ox__Address.Address;
|
||||
@@ -42,7 +42,7 @@ type OnrampPrepareQuoteResponseData = {
|
||||
|
||||
// Explicit type for the API request body
|
||||
interface OnrampApiRequestBody {
|
||||
onramp: "stripe" | "coinbase" | "transak";
|
||||
onramp: "stripe" | "coinbase" | "transak" | "rampnow";
|
||||
chainId: number;
|
||||
tokenAddress: ox__Address.Address;
|
||||
receiver: ox__Address.Address;
|
||||
@@ -128,7 +128,7 @@ interface OnrampApiRequestBody {
|
||||
*
|
||||
* @param options - The options for preparing the onramp.
|
||||
* @param options.client - Your thirdweb client.
|
||||
* @param options.onramp - The onramp provider to use (e.g., "stripe", "coinbase", "transak").
|
||||
* @param options.onramp - The onramp provider to use (e.g., "stripe", "coinbase", "transak", "rampnow").
|
||||
* @param options.chainId - The destination chain ID.
|
||||
* @param options.tokenAddress - The destination token address.
|
||||
* @param options.receiver - The address that will receive the output token.
|
||||
@@ -272,8 +272,8 @@ export declare namespace prepare {
|
||||
export type Options = {
|
||||
/** Your thirdweb client */
|
||||
client: ThirdwebClient;
|
||||
/** The onramp provider to use (e.g., "stripe", "coinbase", "transak") */
|
||||
onramp: "stripe" | "coinbase" | "transak";
|
||||
/** The onramp provider to use (e.g., "stripe", "coinbase", "transak", "rampnow") */
|
||||
onramp: "stripe" | "coinbase" | "transak" | "rampnow";
|
||||
/** The destination chain ID */
|
||||
chainId: number;
|
||||
/** The destination token address */
|
||||
|
||||
+14
-6
@@ -14,14 +14,22 @@ export type ReverseParams = {
|
||||
type: "bytes";
|
||||
name: "reverseName";
|
||||
}>;
|
||||
coinType: AbiParameterToPrimitiveType<{
|
||||
type: "uint256";
|
||||
name: "coinType";
|
||||
}>;
|
||||
};
|
||||
|
||||
export const FN_SELECTOR = "0xec11c823" as const;
|
||||
export const FN_SELECTOR = "0x5d78a217" as const;
|
||||
const FN_INPUTS = [
|
||||
{
|
||||
name: "reverseName",
|
||||
type: "bytes",
|
||||
},
|
||||
{
|
||||
name: "coinType",
|
||||
type: "uint256",
|
||||
},
|
||||
] as const;
|
||||
const FN_OUTPUTS = [
|
||||
{
|
||||
@@ -33,9 +41,6 @@ const FN_OUTPUTS = [
|
||||
{
|
||||
type: "address",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
},
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@@ -70,7 +75,10 @@ export function isReverseSupported(availableSelectors: string[]) {
|
||||
* ```
|
||||
*/
|
||||
export function encodeReverseParams(options: ReverseParams) {
|
||||
return encodeAbiParameters(FN_INPUTS, [options.reverseName]);
|
||||
return encodeAbiParameters(FN_INPUTS, [
|
||||
options.reverseName,
|
||||
options.coinType,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,6 +136,6 @@ export async function reverse(options: BaseTransactionOptions<ReverseParams>) {
|
||||
return readContract({
|
||||
contract: options.contract,
|
||||
method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const,
|
||||
params: [options.reverseName],
|
||||
params: [options.reverseName, options.coinType],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const UNIVERSAL_RESOLVER_ADDRESS =
|
||||
"0xce01f8eee7E479C928F8919abD53E553a36CeF67";
|
||||
"0xeeeeeeee14d718c2b47d9923deab1335e144eeee";
|
||||
export const BASENAME_RESOLVER_ADDRESS =
|
||||
"0xC6d566A56A1aFf6508b41f6c90ff131615583BCD";
|
||||
export const BASE_SEPOLIA_BASENAME_RESOLVER_ADDRESS =
|
||||
|
||||
@@ -3,8 +3,6 @@ import { ethereum } from "../../chains/chain-definitions/ethereum.js";
|
||||
import type { Chain } from "../../chains/types.js";
|
||||
import type { ThirdwebClient } from "../../client/client.js";
|
||||
import { getContract } from "../../contract/contract.js";
|
||||
import { toHex } from "../../utils/encoding/hex.js";
|
||||
import { packetToBytes } from "../../utils/ens/packetToBytes.js";
|
||||
import { withCache } from "../../utils/promise/withCache.js";
|
||||
import { reverse } from "./__generated__/UniversalResolver/read/reverse.js";
|
||||
import { UNIVERSAL_RESOLVER_ADDRESS } from "./constants.js";
|
||||
@@ -33,7 +31,9 @@ export type ResolveNameOptions = {
|
||||
* @extension ENS
|
||||
* @returns A promise that resolves to the Ethereum address.
|
||||
*/
|
||||
export async function resolveName(options: ResolveNameOptions) {
|
||||
export async function resolveName(
|
||||
options: ResolveNameOptions,
|
||||
): Promise<string | null> {
|
||||
const { client, address, resolverAddress, resolverChain } = options;
|
||||
|
||||
return withCache(
|
||||
@@ -44,25 +44,28 @@ export async function resolveName(options: ResolveNameOptions) {
|
||||
client,
|
||||
});
|
||||
|
||||
const reverseName = toHex(
|
||||
packetToBytes(`${address.toLowerCase().substring(2)}.addr.reverse`),
|
||||
);
|
||||
|
||||
const [name, resolvedAddress] = await reverse({
|
||||
const [name] = await reverse({
|
||||
contract,
|
||||
reverseName,
|
||||
reverseName: address as `0x${string}`,
|
||||
coinType: 60n,
|
||||
}).catch((e) => {
|
||||
if ("data" in e && e.data === "0x7199966d") {
|
||||
return [null, address] as const;
|
||||
// Re-throw verification errors so callers can detect data integrity issues
|
||||
if (
|
||||
typeof e === "object" &&
|
||||
e !== null &&
|
||||
"data" in e &&
|
||||
typeof e.data === "string"
|
||||
) {
|
||||
// ReverseAddressMismatch(string,bytes) = 0xef9c03ce
|
||||
if (e.data.startsWith("0xef9c03ce")) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
// Swallow expected "no resolver" / "no name" errors
|
||||
return [null] as const;
|
||||
});
|
||||
|
||||
if (address.toLowerCase() !== resolvedAddress.toLowerCase()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return name;
|
||||
return name || null;
|
||||
},
|
||||
{
|
||||
cacheKey: `ens:name:${resolverChain?.id || 1}:${address}`,
|
||||
|
||||
@@ -290,12 +290,14 @@ export async function getBuyWithFiatQuote(
|
||||
// map preferred provider (FiatProvider) → onramp string expected by Onramp.prepare
|
||||
const mapProviderToOnramp = (
|
||||
provider?: FiatProvider,
|
||||
): "stripe" | "coinbase" | "transak" => {
|
||||
): "stripe" | "coinbase" | "transak" | "rampnow" => {
|
||||
switch (provider) {
|
||||
case "stripe":
|
||||
return "stripe";
|
||||
case "transak":
|
||||
return "transak";
|
||||
case "rampnow":
|
||||
return "rampnow";
|
||||
default: // default to coinbase when undefined or any other value
|
||||
return "coinbase";
|
||||
}
|
||||
@@ -463,7 +465,7 @@ export async function getBuyWithFiatQuote(
|
||||
onRampLink: prepared.link,
|
||||
onRampToken: onRampTokenObject,
|
||||
processingFees: [],
|
||||
provider: (params.preferredProvider ?? "COINBASE") as FiatProvider,
|
||||
provider: params.preferredProvider ?? "coinbase",
|
||||
routingToken: routingTokenObject,
|
||||
toAddress: params.toAddress,
|
||||
toAmountMin: toAmountMin,
|
||||
|
||||
@@ -19,4 +19,4 @@ export type PayOnChainTransactionDetails = {
|
||||
|
||||
export type FiatProvider = (typeof FiatProviders)[number];
|
||||
|
||||
const FiatProviders = ["coinbase", "stripe", "transak"] as const;
|
||||
const FiatProviders = ["coinbase", "stripe", "transak", "rampnow"] as const;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import type React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TEST_CLIENT } from "~test/test-clients.js";
|
||||
import { prepare as prepareOnramp } from "../../../../bridge/Onramp.js";
|
||||
import { getToken } from "../../../../pay/convert/get-token.js";
|
||||
import { useBuyWithFiatQuotesForProviders } from "./useBuyWithFiatQuotesForProviders.js";
|
||||
|
||||
vi.mock("../../../../bridge/Onramp.js");
|
||||
vi.mock("../../../../pay/convert/get-token.js");
|
||||
|
||||
const RECEIVER = "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709" as const;
|
||||
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as const;
|
||||
|
||||
describe("useBuyWithFiatQuotesForProviders", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getToken).mockResolvedValue({
|
||||
address: USDC,
|
||||
chainId: 1,
|
||||
decimals: 6,
|
||||
name: "USD Coin",
|
||||
priceUsd: 1,
|
||||
prices: { USD: 1 },
|
||||
symbol: "USDC",
|
||||
});
|
||||
// Return a minimal valid prepared-onramp response per call.
|
||||
vi.mocked(prepareOnramp).mockImplementation(async (opts) => ({
|
||||
currency: opts.currency ?? "USD",
|
||||
currencyAmount: 1,
|
||||
destinationAmount: opts.amount ?? 1n,
|
||||
destinationToken: {
|
||||
address: opts.tokenAddress,
|
||||
chainId: opts.chainId,
|
||||
decimals: 6,
|
||||
name: "USD Coin",
|
||||
priceUsd: 1,
|
||||
prices: { USD: 1 },
|
||||
symbol: "USDC",
|
||||
},
|
||||
id: `mock-${opts.onramp}`,
|
||||
intent: {
|
||||
amount: (opts.amount ?? 1n).toString(),
|
||||
chainId: opts.chainId,
|
||||
onramp: opts.onramp,
|
||||
receiver: opts.receiver,
|
||||
tokenAddress: opts.tokenAddress,
|
||||
},
|
||||
link: `https://example.com/${opts.onramp}`,
|
||||
steps: [],
|
||||
timestamp: 0,
|
||||
}));
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
it("fans out to all four onramp providers (including rampnow)", async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useBuyWithFiatQuotesForProviders({
|
||||
amount: "10",
|
||||
chainId: 1,
|
||||
client: TEST_CLIENT,
|
||||
country: "US",
|
||||
currency: "USD",
|
||||
receiver: RECEIVER,
|
||||
tokenAddress: USDC,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.every((q) => q.isSuccess)).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current).toHaveLength(4);
|
||||
|
||||
// Each provider should be called exactly once with the expected `onramp` value.
|
||||
const calls = vi.mocked(prepareOnramp).mock.calls.map((c) => c[0].onramp);
|
||||
expect(calls.sort()).toEqual(
|
||||
["coinbase", "rampnow", "stripe", "transak"].sort(),
|
||||
);
|
||||
|
||||
// The hook should surface the prepared link per provider.
|
||||
const links = result.current.map((q) => q.data?.link);
|
||||
expect(links).toContain("https://example.com/rampnow");
|
||||
expect(links).toContain("https://example.com/stripe");
|
||||
expect(links).toContain("https://example.com/coinbase");
|
||||
expect(links).toContain("https://example.com/transak");
|
||||
});
|
||||
|
||||
it("is disabled when no params are provided", () => {
|
||||
const { result } = renderHook(() => useBuyWithFiatQuotesForProviders(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toHaveLength(4);
|
||||
expect(result.current.every((q) => !q.isSuccess && !q.isError)).toBe(true);
|
||||
expect(vi.mocked(prepareOnramp)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -60,13 +60,13 @@ type UseBuyWithFiatQuotesForProvidersResult = {
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Hook to get prepared onramp quotes from Coinbase, Stripe, and Transak providers.
|
||||
* Hook to get prepared onramp quotes from Coinbase, Stripe, Transak, and Rampnow providers.
|
||||
*/
|
||||
export function useBuyWithFiatQuotesForProviders(
|
||||
params?: UseBuyWithFiatQuotesForProvidersParams,
|
||||
queryOptions?: OnrampQuoteQueryOptions,
|
||||
): UseBuyWithFiatQuotesForProvidersResult {
|
||||
const providers = ["coinbase", "stripe", "transak"] as const;
|
||||
const providers = ["coinbase", "stripe", "transak", "rampnow"] as const;
|
||||
|
||||
const queries = useQueries({
|
||||
queries: providers.map((provider) => ({
|
||||
|
||||
@@ -81,6 +81,23 @@ describe("useBridgePrepare", () => {
|
||||
expect(onrampRequest.client).toBe(mockClient);
|
||||
});
|
||||
|
||||
it("should accept all supported onramp providers including rampnow", () => {
|
||||
const providers = ["stripe", "coinbase", "transak", "rampnow"] as const;
|
||||
for (const onramp of providers) {
|
||||
const onrampRequest: BridgePrepareRequest = {
|
||||
amount: 1000000n,
|
||||
chainId: 1,
|
||||
client: mockClient,
|
||||
onramp,
|
||||
receiver: "0x1234567890123456789012345678901234567890",
|
||||
tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
type: "onramp",
|
||||
};
|
||||
expect(onrampRequest.type).toBe("onramp");
|
||||
expect(onrampRequest.onramp).toBe(onramp);
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle UseBridgePrepareParams with enabled option", () => {
|
||||
const params: UseBridgePrepareParams = {
|
||||
amount: 1000000n,
|
||||
|
||||
+9
-1
@@ -21,7 +21,9 @@ import { Text } from "../../components/text.js";
|
||||
|
||||
interface FiatProviderSelectionProps {
|
||||
client: ThirdwebClient;
|
||||
onProviderSelected: (provider: "coinbase" | "stripe" | "transak") => void;
|
||||
onProviderSelected: (
|
||||
provider: "coinbase" | "stripe" | "transak" | "rampnow",
|
||||
) => void;
|
||||
toChainId: number;
|
||||
toTokenAddress: string;
|
||||
toAddress: string;
|
||||
@@ -49,6 +51,12 @@ const PROVIDERS = [
|
||||
id: "transak" as const,
|
||||
name: "Transak",
|
||||
},
|
||||
{
|
||||
description: "Cards, bank transfers and more",
|
||||
iconUri: "https://app.rampnow.io/favicon.ico",
|
||||
id: "rampnow" as const,
|
||||
name: "Rampnow",
|
||||
},
|
||||
];
|
||||
|
||||
export function FiatProviderSelection({
|
||||
|
||||
@@ -184,7 +184,7 @@ export function PaymentSelection({
|
||||
};
|
||||
|
||||
const handleOnrampProviderSelected = (
|
||||
provider: "coinbase" | "stripe" | "transak",
|
||||
provider: "coinbase" | "stripe" | "transak" | "rampnow",
|
||||
) => {
|
||||
const recipientAddress =
|
||||
receiverAddress || payerWallet?.getAccount()?.address;
|
||||
|
||||
@@ -44,5 +44,5 @@ export type PaymentMethod =
|
||||
type: "fiat";
|
||||
payerWallet?: Wallet;
|
||||
currency: SupportedFiatCurrency;
|
||||
onramp: "stripe" | "coinbase" | "transak";
|
||||
onramp: "stripe" | "coinbase" | "transak" | "rampnow";
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# @thirdweb-dev/wagmi-adapter
|
||||
|
||||
## 0.2.212
|
||||
|
||||
## 0.2.211
|
||||
|
||||
## 0.2.210
|
||||
|
||||
## 0.2.209
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"type": "module",
|
||||
"types": "./dist/types/exports/thirdweb.d.ts",
|
||||
"typings": "./dist/types/exports/thirdweb.d.ts",
|
||||
"version": "0.2.210"
|
||||
"version": "0.2.212"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user