Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
a82e63319a Version Packages (#8765)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-12 05:21:51 +07:00
FirekeeperandGitHub 23d74a0a54 Add Rampnow as a fiat onramp provider (#8764) 2026-05-12 04:54:07 +07:00
FirekeeperandGitHub eb07340118 Dedicated channel for Pro plan (#8751) 2026-04-22 00:39:29 +07:00
FirekeeperandGitHub dc07145543 Handle managed vault rotation and redirect (#8747) 2026-04-21 02:28:20 +07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
901789983f Version Packages (#8734)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-06 23:21:42 +07:00
FirekeeperandGitHub ad49560331 ENS: add coinType param to reverse resolver (#8735) 2026-04-06 23:10:59 +07:00
83198b2ea4 [SDK] Update ENS Universal Resolver address for ENSv2 (#8733)
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: 0xFirekeeper <[email protected]>
2026-04-06 22:07:53 +07:00
29 changed files with 378 additions and 119 deletions
+32 -14
View File
@@ -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;
+17 -18
View File
@@ -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:",
@@ -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
@@ -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={
@@ -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]">
@@ -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>
@@ -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>
@@ -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);
+16
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wagmi-inapp",
"private": true,
"version": "0.0.55",
"version": "0.0.57",
"type": "module",
"scripts": {
"dev": "vite",
+14
View File
@@ -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
+1 -1
View File
@@ -57,5 +57,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.113"
"version": "0.2.115"
}
+14
View File
@@ -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
+1 -1
View File
@@ -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);
},
);
});
+5 -5
View File
@@ -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,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,
@@ -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";
};
+4
View File
@@ -1,5 +1,9 @@
# @thirdweb-dev/wagmi-adapter
## 0.2.212
## 0.2.211
## 0.2.210
## 0.2.209
+1 -1
View File
@@ -56,5 +56,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.210"
"version": "0.2.212"
}