Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chatty-peaches-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": minor
---

[SDK] Add RampNow as a new onramp provider
29 changes: 29 additions & 0 deletions packages/thirdweb/src/bridge/Onramp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
);
});
10 changes: 5 additions & 5 deletions packages/thirdweb/src/bridge/Onramp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 */
Expand Down
6 changes: 4 additions & 2 deletions packages/thirdweb/src/pay/buyWithFiat/getQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/thirdweb/src/pay/utils/commonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@

/**
* @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;

Check warning on line 69 in packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.ts#L69

Added line #L69 was not covered by tests

const queries = useQueries({
queries: providers.map((provider) => ({
Expand Down
17 changes: 17 additions & 0 deletions packages/thirdweb/src/react/core/hooks/useBridgePrepare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
};

const handleOnrampProviderSelected = (
provider: "coinbase" | "stripe" | "transak",
provider: "coinbase" | "stripe" | "transak" | "rampnow",

Check warning on line 187 in packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx#L187

Added line #L187 was not covered by tests
) => {
const recipientAddress =
receiverAddress || payerWallet?.getAccount()?.address;
Expand Down
2 changes: 1 addition & 1 deletion packages/thirdweb/src/react/web/ui/Bridge/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ export type PaymentMethod =
type: "fiat";
payerWallet?: Wallet;
currency: SupportedFiatCurrency;
onramp: "stripe" | "coinbase" | "transak";
onramp: "stripe" | "coinbase" | "transak" | "rampnow";
};
Loading