Skip to content

Commit 23d74a0

Browse files
authored
Add Rampnow as a fiat onramp provider (#8764)
1 parent eb07340 commit 23d74a0

11 files changed

Lines changed: 183 additions & 13 deletions

File tree

.changeset/chatty-peaches-drop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
[SDK] Add RampNow as a new onramp provider

packages/thirdweb/src/bridge/Onramp.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,33 @@ describe.runIf(process.env.TW_SECRET_KEY)("Bridge.Onramp.prepare", () => {
115115
// Steps array should be defined (it may be empty if the provider supports the destination token natively)
116116
expect(Array.isArray(prepared.steps)).toBe(true);
117117
});
118+
119+
// The Rampnow live-API test is gated on a separate env var because the
120+
// server-side schema for `onramp: "rampnow"` lands in a separate deploy.
121+
// Once `api.thirdweb-dev.com` accepts the new provider, drop the runIf and
122+
// mirror the stripe/coinbase/transak assertions above.
123+
it.runIf(process.env.TW_BRIDGE_RAMPNOW)(
124+
"should prepare a Rampnow onramp successfully",
125+
async () => {
126+
const prepared = await Onramp.prepare({
127+
amount: toWei("0.01"),
128+
chainId: 1,
129+
client: TEST_CLIENT,
130+
onramp: "rampnow",
131+
receiver: RECEIVER_ADDRESS,
132+
tokenAddress: NATIVE_TOKEN_ADDRESS,
133+
});
134+
135+
expect(prepared).toBeDefined();
136+
expect(typeof prepared.destinationAmount).toBe("bigint");
137+
expect(prepared.destinationAmount > 0n).toBe(true);
138+
expect(prepared.link).toBeDefined();
139+
expect(typeof prepared.link).toBe("string");
140+
expect(prepared.intent).toBeDefined();
141+
expect(prepared.intent.receiver.toLowerCase()).toBe(
142+
RECEIVER_ADDRESS.toLowerCase(),
143+
);
144+
expect(Array.isArray(prepared.steps)).toBe(true);
145+
},
146+
);
118147
});

packages/thirdweb/src/bridge/Onramp.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { TokenWithPrices } from "./types/Token.js";
1313
export { status } from "./OnrampStatus.js";
1414

1515
type OnrampIntent = {
16-
onramp: "stripe" | "coinbase" | "transak";
16+
onramp: "stripe" | "coinbase" | "transak" | "rampnow";
1717
chainId: number;
1818
tokenAddress: ox__Address.Address;
1919
receiver: ox__Address.Address;
@@ -42,7 +42,7 @@ type OnrampPrepareQuoteResponseData = {
4242

4343
// Explicit type for the API request body
4444
interface OnrampApiRequestBody {
45-
onramp: "stripe" | "coinbase" | "transak";
45+
onramp: "stripe" | "coinbase" | "transak" | "rampnow";
4646
chainId: number;
4747
tokenAddress: ox__Address.Address;
4848
receiver: ox__Address.Address;
@@ -128,7 +128,7 @@ interface OnrampApiRequestBody {
128128
*
129129
* @param options - The options for preparing the onramp.
130130
* @param options.client - Your thirdweb client.
131-
* @param options.onramp - The onramp provider to use (e.g., "stripe", "coinbase", "transak").
131+
* @param options.onramp - The onramp provider to use (e.g., "stripe", "coinbase", "transak", "rampnow").
132132
* @param options.chainId - The destination chain ID.
133133
* @param options.tokenAddress - The destination token address.
134134
* @param options.receiver - The address that will receive the output token.
@@ -272,8 +272,8 @@ export declare namespace prepare {
272272
export type Options = {
273273
/** Your thirdweb client */
274274
client: ThirdwebClient;
275-
/** The onramp provider to use (e.g., "stripe", "coinbase", "transak") */
276-
onramp: "stripe" | "coinbase" | "transak";
275+
/** The onramp provider to use (e.g., "stripe", "coinbase", "transak", "rampnow") */
276+
onramp: "stripe" | "coinbase" | "transak" | "rampnow";
277277
/** The destination chain ID */
278278
chainId: number;
279279
/** The destination token address */

packages/thirdweb/src/pay/buyWithFiat/getQuote.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,14 @@ export async function getBuyWithFiatQuote(
290290
// map preferred provider (FiatProvider) → onramp string expected by Onramp.prepare
291291
const mapProviderToOnramp = (
292292
provider?: FiatProvider,
293-
): "stripe" | "coinbase" | "transak" => {
293+
): "stripe" | "coinbase" | "transak" | "rampnow" => {
294294
switch (provider) {
295295
case "stripe":
296296
return "stripe";
297297
case "transak":
298298
return "transak";
299+
case "rampnow":
300+
return "rampnow";
299301
default: // default to coinbase when undefined or any other value
300302
return "coinbase";
301303
}
@@ -463,7 +465,7 @@ export async function getBuyWithFiatQuote(
463465
onRampLink: prepared.link,
464466
onRampToken: onRampTokenObject,
465467
processingFees: [],
466-
provider: (params.preferredProvider ?? "COINBASE") as FiatProvider,
468+
provider: params.preferredProvider ?? "coinbase",
467469
routingToken: routingTokenObject,
468470
toAddress: params.toAddress,
469471
toAmountMin: toAmountMin,

packages/thirdweb/src/pay/utils/commonTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ export type PayOnChainTransactionDetails = {
1919

2020
export type FiatProvider = (typeof FiatProviders)[number];
2121

22-
const FiatProviders = ["coinbase", "stripe", "transak"] as const;
22+
const FiatProviders = ["coinbase", "stripe", "transak", "rampnow"] as const;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2+
import { renderHook, waitFor } from "@testing-library/react";
3+
import type React from "react";
4+
import { beforeEach, describe, expect, it, vi } from "vitest";
5+
import { TEST_CLIENT } from "~test/test-clients.js";
6+
import { prepare as prepareOnramp } from "../../../../bridge/Onramp.js";
7+
import { getToken } from "../../../../pay/convert/get-token.js";
8+
import { useBuyWithFiatQuotesForProviders } from "./useBuyWithFiatQuotesForProviders.js";
9+
10+
vi.mock("../../../../bridge/Onramp.js");
11+
vi.mock("../../../../pay/convert/get-token.js");
12+
13+
const RECEIVER = "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709" as const;
14+
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as const;
15+
16+
describe("useBuyWithFiatQuotesForProviders", () => {
17+
beforeEach(() => {
18+
vi.clearAllMocks();
19+
vi.mocked(getToken).mockResolvedValue({
20+
address: USDC,
21+
chainId: 1,
22+
decimals: 6,
23+
name: "USD Coin",
24+
priceUsd: 1,
25+
prices: { USD: 1 },
26+
symbol: "USDC",
27+
});
28+
// Return a minimal valid prepared-onramp response per call.
29+
vi.mocked(prepareOnramp).mockImplementation(async (opts) => ({
30+
currency: opts.currency ?? "USD",
31+
currencyAmount: 1,
32+
destinationAmount: opts.amount ?? 1n,
33+
destinationToken: {
34+
address: opts.tokenAddress,
35+
chainId: opts.chainId,
36+
decimals: 6,
37+
name: "USD Coin",
38+
priceUsd: 1,
39+
prices: { USD: 1 },
40+
symbol: "USDC",
41+
},
42+
id: `mock-${opts.onramp}`,
43+
intent: {
44+
amount: (opts.amount ?? 1n).toString(),
45+
chainId: opts.chainId,
46+
onramp: opts.onramp,
47+
receiver: opts.receiver,
48+
tokenAddress: opts.tokenAddress,
49+
},
50+
link: `https://example.com/${opts.onramp}`,
51+
steps: [],
52+
timestamp: 0,
53+
}));
54+
});
55+
56+
const wrapper = ({ children }: { children: React.ReactNode }) => {
57+
const queryClient = new QueryClient({
58+
defaultOptions: { queries: { retry: false } },
59+
});
60+
return (
61+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
62+
);
63+
};
64+
65+
it("fans out to all four onramp providers (including rampnow)", async () => {
66+
const { result } = renderHook(
67+
() =>
68+
useBuyWithFiatQuotesForProviders({
69+
amount: "10",
70+
chainId: 1,
71+
client: TEST_CLIENT,
72+
country: "US",
73+
currency: "USD",
74+
receiver: RECEIVER,
75+
tokenAddress: USDC,
76+
}),
77+
{ wrapper },
78+
);
79+
80+
await waitFor(() => {
81+
expect(result.current.every((q) => q.isSuccess)).toBe(true);
82+
});
83+
84+
expect(result.current).toHaveLength(4);
85+
86+
// Each provider should be called exactly once with the expected `onramp` value.
87+
const calls = vi.mocked(prepareOnramp).mock.calls.map((c) => c[0].onramp);
88+
expect(calls.sort()).toEqual(
89+
["coinbase", "rampnow", "stripe", "transak"].sort(),
90+
);
91+
92+
// The hook should surface the prepared link per provider.
93+
const links = result.current.map((q) => q.data?.link);
94+
expect(links).toContain("https://example.com/rampnow");
95+
expect(links).toContain("https://example.com/stripe");
96+
expect(links).toContain("https://example.com/coinbase");
97+
expect(links).toContain("https://example.com/transak");
98+
});
99+
100+
it("is disabled when no params are provided", () => {
101+
const { result } = renderHook(() => useBuyWithFiatQuotesForProviders(), {
102+
wrapper,
103+
});
104+
105+
expect(result.current).toHaveLength(4);
106+
expect(result.current.every((q) => !q.isSuccess && !q.isError)).toBe(true);
107+
expect(vi.mocked(prepareOnramp)).not.toHaveBeenCalled();
108+
});
109+
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@ type UseBuyWithFiatQuotesForProvidersResult = {
6060

6161
/**
6262
* @internal
63-
* Hook to get prepared onramp quotes from Coinbase, Stripe, and Transak providers.
63+
* Hook to get prepared onramp quotes from Coinbase, Stripe, Transak, and Rampnow providers.
6464
*/
6565
export function useBuyWithFiatQuotesForProviders(
6666
params?: UseBuyWithFiatQuotesForProvidersParams,
6767
queryOptions?: OnrampQuoteQueryOptions,
6868
): UseBuyWithFiatQuotesForProvidersResult {
69-
const providers = ["coinbase", "stripe", "transak"] as const;
69+
const providers = ["coinbase", "stripe", "transak", "rampnow"] as const;
7070

7171
const queries = useQueries({
7272
queries: providers.map((provider) => ({

packages/thirdweb/src/react/core/hooks/useBridgePrepare.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,23 @@ describe("useBridgePrepare", () => {
8181
expect(onrampRequest.client).toBe(mockClient);
8282
});
8383

84+
it("should accept all supported onramp providers including rampnow", () => {
85+
const providers = ["stripe", "coinbase", "transak", "rampnow"] as const;
86+
for (const onramp of providers) {
87+
const onrampRequest: BridgePrepareRequest = {
88+
amount: 1000000n,
89+
chainId: 1,
90+
client: mockClient,
91+
onramp,
92+
receiver: "0x1234567890123456789012345678901234567890",
93+
tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
94+
type: "onramp",
95+
};
96+
expect(onrampRequest.type).toBe("onramp");
97+
expect(onrampRequest.onramp).toBe(onramp);
98+
}
99+
});
100+
84101
it("should handle UseBridgePrepareParams with enabled option", () => {
85102
const params: UseBridgePrepareParams = {
86103
amount: 1000000n,

packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import { Text } from "../../components/text.js";
2121

2222
interface FiatProviderSelectionProps {
2323
client: ThirdwebClient;
24-
onProviderSelected: (provider: "coinbase" | "stripe" | "transak") => void;
24+
onProviderSelected: (
25+
provider: "coinbase" | "stripe" | "transak" | "rampnow",
26+
) => void;
2527
toChainId: number;
2628
toTokenAddress: string;
2729
toAddress: string;
@@ -49,6 +51,12 @@ const PROVIDERS = [
4951
id: "transak" as const,
5052
name: "Transak",
5153
},
54+
{
55+
description: "Cards, bank transfers and more",
56+
iconUri: "https://app.rampnow.io/favicon.ico",
57+
id: "rampnow" as const,
58+
name: "Rampnow",
59+
},
5260
];
5361

5462
export function FiatProviderSelection({

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export function PaymentSelection({
184184
};
185185

186186
const handleOnrampProviderSelected = (
187-
provider: "coinbase" | "stripe" | "transak",
187+
provider: "coinbase" | "stripe" | "transak" | "rampnow",
188188
) => {
189189
const recipientAddress =
190190
receiverAddress || payerWallet?.getAccount()?.address;

0 commit comments

Comments
 (0)