Skip to content

Commit c4ecf20

Browse files
committed
initial commit to implement fiat onramp into @turnkey/core and @turnkey/react-wallet-kit
1 parent 51d29b0 commit c4ecf20

File tree

12 files changed

+651
-302
lines changed

12 files changed

+651
-302
lines changed
1.18 KB
Loading
Lines changed: 1 addition & 0 deletions
Loading
1.48 KB
Loading
Lines changed: 15 additions & 0 deletions
Loading

examples/react-wallet-kit/src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export default function AuthPage() {
7171
</div>
7272
) : (
7373
<TabGroup
74-
onChange={(index) => setSelectedTabIndex(index)}
74+
onChange={(index: number) => setSelectedTabIndex(index)}
7575
selectedIndex={selectedTabIndex}
7676
>
7777
<TabPanels className="flex justify-center items-center">

examples/react-wallet-kit/src/components/Svg.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,45 @@ export function ImportSVG(props: SVGProps) {
314314
</svg>
315315
);
316316
}
317+
318+
export function MoonPaySVG(props: SVGProps) {
319+
return (
320+
<svg
321+
xmlns="http://www.w3.org/2000/svg"
322+
width="16"
323+
height="16"
324+
viewBox="0 0 16 16"
325+
>
326+
<circle cx="8" cy="8" r="9" fill="#ffffff" />
327+
<rect x="0" y="0" width="16" height="16" fill="none" />
328+
<rect x="12" y="0" width="3" height="1" fill="#7d00ff" />
329+
<rect x="11" y="1" width="5" height="1" fill="#7d00ff" />
330+
<rect x="10" y="2" width="6" height="1" fill="#7d00ff" />
331+
<rect x="11" y="3" width="5" height="1" fill="#7d00ff" />
332+
<rect x="3" y="4" width="6" height="1" fill="#7d00ff" />
333+
<rect x="11" y="4" width="4" height="1" fill="#7d00ff" />
334+
<rect x="2" y="5" width="8" height="1" fill="#7d00ff" />
335+
<rect x="1" y="6" width="10" height="1" fill="#7d00ff" />
336+
<rect x="0" y="7" width="12" height="5" fill="#7d00ff" />
337+
<rect x="1" y="12" width="10" height="2" fill="#7d00ff" />
338+
<rect x="2" y="14" width="8" height="1" fill="#7d00ff" />
339+
<rect x="4" y="15" width="4" height="1" fill="#7d00ff" />
340+
</svg>
341+
);
342+
}
343+
344+
export function CoinbaseSVG(props: SVGProps) {
345+
return (
346+
<svg
347+
xmlns="http://www.w3.org/2000/svg"
348+
fill="currentColor"
349+
viewBox="0 0 32 32"
350+
{...props}
351+
>
352+
<path
353+
fill="#0052FF"
354+
d="M16 23c-3.867 0-7-3.133-7-7s3.133-7 7-7c3.465 0 6.34 2.526 6.895 5.833h7.053C29.352 7.647 23.338 2 16 2 8.27 2 2 8.27 2 16s6.27 14 14 14 13.352-5.647 13.948-12.833h-7.053A6.993 6.993 0 0 1 16 23Z"
355+
/>
356+
</svg>
357+
);
358+
}

examples/react-wallet-kit/src/components/demo/DemoPanel.tsx

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
} from "@headlessui/react";
1515
import {
1616
ConnectedWallet,
17-
ExportType,
1817
useModal,
1918
useTurnkey,
2019
Wallet,
@@ -40,6 +39,7 @@ import {
4039
} from "@/utils";
4140
import SignatureVerification from "./SignatureVerification";
4241
import Image from "next/image";
42+
import OnrampSelector from "./OnrampSelector";
4343

4444
export default function DemoPanel() {
4545
const {
@@ -57,7 +57,7 @@ export default function DemoPanel() {
5757
const { pushPage } = useModal();
5858

5959
const [selectedWallet, setSelectedWallet] = useState<Wallet | undefined>(
60-
wallets[0] || null, // Initialize with null if wallets[0] is undefined
60+
wallets[0] || null // Initialize with null if wallets[0] is undefined
6161
);
6262
const [selectedWalletAccount, setSelectedWalletAccount] = useState<
6363
WalletAccount | undefined
@@ -67,7 +67,7 @@ export default function DemoPanel() {
6767
ConnectedWallet[] | undefined
6868
>([]); // Initialize with an empty array
6969
const [connectedWalletIcons, setConnectedWalletIcons] = useState<string[]>(
70-
[],
70+
[]
7171
); // Initialize with an empty array
7272

7373
useEffect(() => {
@@ -95,7 +95,7 @@ export default function DemoPanel() {
9595
}
9696

9797
const cw = wallets.filter(
98-
(w) => w.source === WalletSource.Connected,
98+
(w) => w.source === WalletSource.Connected
9999
) as ConnectedWallet[];
100100
if (cw) {
101101
getConnectedWalletIcons().then((icons) => {
@@ -161,7 +161,7 @@ export default function DemoPanel() {
161161
onClick={() => {
162162
setSelectedWallet(wallet);
163163
setSelectedWalletAccount(
164-
wallet.accounts[0] || undefined,
164+
wallet.accounts[0] || undefined
165165
);
166166
}}
167167
className="flex items-center gap-3 w-full cursor-pointer"
@@ -184,7 +184,7 @@ export default function DemoPanel() {
184184
<Button
185185
onClick={async () => {
186186
const embeddedWallets = wallets.filter(
187-
(w) => w.source === WalletSource.Embedded,
187+
(w) => w.source === WalletSource.Embedded
188188
);
189189
const walletId = await createWallet({
190190
walletName: `Wallet ${embeddedWallets.length + 1}`,
@@ -199,7 +199,7 @@ export default function DemoPanel() {
199199
newWallets[0];
200200
setSelectedWallet(newWallet);
201201
setSelectedWalletAccount(
202-
newWallet.accounts[0] || undefined,
202+
newWallet.accounts[0] || undefined
203203
);
204204
}}
205205
className="relative hover:cursor-pointer flex items-center justify-center gap-2 w-full px-3 py-2 rounded-md text-xs bg-icon-background-light dark:bg-icon-background-dark text-icon-text-light dark:text-icon-text-dark"
@@ -278,7 +278,7 @@ export default function DemoPanel() {
278278
account.addressFormat === "ADDRESS_FORMAT_ETHEREUM"
279279
? `https://etherscan.io/address/${account.address}`
280280
: `https://solscan.io/account/${account.address}`,
281-
"_blank",
281+
"_blank"
282282
);
283283
}}
284284
>
@@ -337,13 +337,13 @@ export default function DemoPanel() {
337337
res.r,
338338
res.s,
339339
res.v,
340-
selectedWalletAccount.address,
340+
selectedWalletAccount.address
341341
)
342342
: verifySolSignatureWithAddress(
343343
messageToSign,
344344
res.r,
345345
res.s,
346-
selectedWalletAccount.address,
346+
selectedWalletAccount.address
347347
);
348348
pushPage({
349349
key: "Signature Verification",
@@ -361,6 +361,26 @@ export default function DemoPanel() {
361361
>
362362
Sign Message
363363
</Button>
364+
365+
<Button
366+
onClick={async () => {
367+
if (!selectedWalletAccount) return;
368+
pushPage({
369+
key: "Onramp Selector",
370+
content: (
371+
<OnrampSelector
372+
selectedWalletAccount={selectedWalletAccount}
373+
/>
374+
),
375+
preventBack: true,
376+
showTitle: false,
377+
});
378+
}}
379+
className="bg-primary-light dark:bg-primary-dark text-primary-text-light dark:text-primary-text-dark rounded-lg px-4 py-2 active:scale-95 transition-transform cursor-pointer"
380+
>
381+
<FontAwesomeIcon icon={faPlus} className="w-4 h-4 mr-2" /> Add Funds
382+
</Button>
383+
364384
{selectedWallet?.source === WalletSource.Embedded && (
365385
<>
366386
<hr className="border-draggable-background-light dark:border-draggable-background-dark" />
@@ -422,7 +442,7 @@ const StackedImg = ({
422442
alt={`Wallet Icon ${index}`}
423443
className={`w-6 h-6 bg-icon-background-light dark:bg-icon-background-dark rounded-full p-0.5 ${index > 0 ? "-ml-3" : ""}`}
424444
/>
425-
) : null,
445+
) : null
426446
)}
427447
{connectedWalletIcons.length > 2 && (
428448
<span className="text-xs text-text-light dark:text-text-dark">
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"use client";
2+
3+
import { Button } from "@headlessui/react";
4+
import { useTurnkey, WalletAccount } from "@turnkey/react-wallet-kit";
5+
import {
6+
FiatOnRampBlockchainNetwork,
7+
FiatOnRampCryptoCurrency,
8+
FiatOnRampCurrency,
9+
FiatOnRampPaymentMethod,
10+
FiatOnRampProvider,
11+
} from "@turnkey/sdk-types";
12+
import { CoinbaseSVG, MoonPaySVG, SolanaSVG } from "../Svg";
13+
export default function OnrampSelector({
14+
selectedWalletAccount,
15+
}: {
16+
selectedWalletAccount: WalletAccount;
17+
}) {
18+
const { initFiatOnramp, session } = useTurnkey();
19+
20+
const generateCoinbaseUrl = async () => {
21+
try {
22+
if (!session) {
23+
throw new Error("Session not found");
24+
}
25+
26+
const response = await initFiatOnramp({
27+
organizationId: session?.organizationId!,
28+
onrampProvider: FiatOnRampProvider.COINBASE,
29+
walletAddress: selectedWalletAccount.address,
30+
network: FiatOnRampBlockchainNetwork.ETHEREUM,
31+
cryptoCurrencyCode: FiatOnRampCryptoCurrency.ETHEREUM,
32+
fiatCurrencyCode: FiatOnRampCurrency.USD,
33+
fiatCurrencyAmount: "10",
34+
paymentMethod: FiatOnRampPaymentMethod.CREDIT_DEBIT_CARD,
35+
countryCode: "US",
36+
countrySubdivisionCode: "ME",
37+
sandboxMode: true,
38+
});
39+
40+
if (response?.onRampUrl) {
41+
window.open(
42+
response.onRampUrl,
43+
"_blank",
44+
"popup,width=500,height=700,scrollbars=yes,resizable=yes"
45+
);
46+
}
47+
} catch (error) {
48+
console.error("Failed to init Coinbase on-ramp:", error);
49+
}
50+
};
51+
52+
const generateMoonPayUrl = async () => {
53+
try {
54+
if (!session) {
55+
throw new Error("Session not found");
56+
}
57+
58+
const response = await initFiatOnramp({
59+
organizationId: session?.organizationId!,
60+
onrampProvider: FiatOnRampProvider.MOONPAY,
61+
walletAddress: selectedWalletAccount.address,
62+
network: FiatOnRampBlockchainNetwork.ETHEREUM,
63+
cryptoCurrencyCode: FiatOnRampCryptoCurrency.ETHEREUM,
64+
fiatCurrencyCode: FiatOnRampCurrency.USD,
65+
fiatCurrencyAmount: "10",
66+
paymentMethod: FiatOnRampPaymentMethod.CREDIT_DEBIT_CARD,
67+
sandboxMode: true,
68+
});
69+
70+
if (response?.onRampUrl) {
71+
window.open(
72+
response.onRampUrl,
73+
"_blank",
74+
"popup,width=500,height=700,scrollbars=yes,resizable=yes"
75+
);
76+
}
77+
} catch (error) {
78+
console.error("Failed to init MoonPay on-ramp:", error);
79+
}
80+
};
81+
82+
return (
83+
<div className="flex flex-col justify-center w-72 p-2">
84+
<div
85+
className="flex flex-col items-center justify-center"
86+
style={{ height: "auto", width: "auto" }}
87+
>
88+
<h2>Add funds to your wallet</h2>
89+
<p className="text-xs text-icon-text-light dark:text-icon-text-dark">
90+
Your crypto will be deposited directly into your Turnkey wallet
91+
</p>
92+
<Button
93+
onClick={async () => await generateCoinbaseUrl()}
94+
className="bg-primary-light dark:bg-primary-dark text-primary-text-light dark:text-primary-text-dark rounded-lg px-10 py-2 active:scale-95 transition-transform cursor-pointer"
95+
>
96+
<CoinbaseSVG className="w-8 h-8" />
97+
Buy with Coinbase
98+
</Button>
99+
<Button
100+
onClick={async () => generateMoonPayUrl()}
101+
className="bg-primary-light dark:bg-primary-dark text-primary-text-light dark:text-primary-text-dark rounded-lg px-4 py-2 active:scale-95 transition-transform cursor-pointer"
102+
>
103+
<MoonPaySVG className="w-8 h-8" />
104+
Buy With MoonPay
105+
</Button>
106+
</div>
107+
</div>
108+
);
109+
}

packages/core/src/__clients__/core.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
v1PayloadEncoding,
1818
v1HashFunction,
1919
v1Curve,
20+
TInitFiatOnRampBody,
2021
} from "@turnkey/sdk-types";
2122
import {
2223
DEFAULT_SESSION_EXPIRATION_IN_SECONDS,
@@ -1081,6 +1082,44 @@ export class TurnkeyClient {
10811082
);
10821083
};
10831084

1085+
/**
1086+
* Initializes the Fiat Onramp Flow.
1087+
*
1088+
* - This function initiates the OTP flow by sending a one-time password (OTP) code to the user's contact information (email address or phone number) via the auth proxy.
1089+
* - Supports both email and SMS OTP types.
1090+
* - Returns an OTP ID that is required for subsequent OTP verification.
1091+
*
1092+
* @param params.otpType - type of OTP to initialize (OtpType.Email or OtpType.Sms).
1093+
* @param params.contact - contact information for the user (e.g., email address or phone number).
1094+
* @returns A promise that resolves with the onRampUrl and onRampTransactionId.
1095+
* @throws {TurnkeyError} If there is an error during the fiat onramp initialization process.
1096+
*/
1097+
initFiatOnramp = async (
1098+
params: TInitFiatOnRampBody,
1099+
): Promise<{ onRampUrl: string; onRampTransactionId: string }> => {
1100+
return withTurnkeyErrorHandling(
1101+
async () => {
1102+
const initFiatOnRampRes = await this.httpClient.initFiatOnRamp(params);
1103+
1104+
if (!initFiatOnRampRes || !initFiatOnRampRes.onRampUrl) {
1105+
throw new TurnkeyError(
1106+
"Failed to initialize fiat onramp: onRampUrl is missing",
1107+
TurnkeyErrorCodes.INIT_FIAT_ONRAMP_ERROR,
1108+
);
1109+
}
1110+
1111+
return {
1112+
onRampUrl: initFiatOnRampRes.onRampUrl,
1113+
onRampTransactionId: initFiatOnRampRes.onRampTransactionId,
1114+
};
1115+
},
1116+
{
1117+
errorMessage: "Failed to initialize fiat onramp",
1118+
errorCode: TurnkeyErrorCodes.INIT_FIAT_ONRAMP_ERROR,
1119+
},
1120+
);
1121+
};
1122+
10841123
/**
10851124
* Initializes the OTP process by sending an OTP code to the provided contact.
10861125
*

0 commit comments

Comments
 (0)