@coinbase/create-cdp-app 0.0.38 → 0.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/index.js +99 -38
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/template-nextjs/README.md +1 -1
  5. package/template-nextjs/src/app/api/onramp/buy-options/route.ts +0 -2
  6. package/template-nextjs/src/components/EOATransaction.tsx +7 -7
  7. package/template-nextjs/src/components/FundWallet.tsx +17 -3
  8. package/template-nextjs/src/components/SignedInScreen.tsx +2 -2
  9. package/template-nextjs/src/components/SignedInScreenWithOnramp.tsx +149 -41
  10. package/template-nextjs/src/components/SolanaTransaction.tsx +106 -96
  11. package/template-nextjs/src/lib/onramp-api.ts +6 -2
  12. package/template-react/src/EOATransaction.tsx +7 -7
  13. package/template-react/src/SignedInScreen.tsx +2 -3
  14. package/template-react/src/SolanaTransaction.tsx +105 -96
  15. package/template-react-native/App.tsx +28 -6
  16. package/template-react-native/SolanaTransaction.tsx +368 -0
  17. package/template-react-native/Transaction.tsx +101 -1
  18. package/template-react-native/_gitignore +4 -0
  19. package/template-react-native/components/WalletHeader.tsx +37 -13
  20. package/template-react-native/env.example +1 -0
  21. package/template-react-native/index.ts +7 -0
  22. package/template-react-native/metro.config.js +19 -0
  23. package/template-react-native/package.json +3 -0
  24. package/template-react-native/android/_gitignore +0 -16
  25. package/template-react-native/android/app/build.gradle +0 -177
  26. package/template-react-native/android/app/debug.keystore +0 -0
  27. package/template-react-native/android/app/proguard-rules.pro +0 -14
  28. package/template-react-native/android/app/src/debug/AndroidManifest.xml +0 -7
  29. package/template-react-native/android/app/src/main/AndroidManifest.xml +0 -25
  30. package/template-react-native/android/app/src/main/java/com/anonymous/reactnativeexpo/MainActivity.kt +0 -61
  31. package/template-react-native/android/app/src/main/java/com/anonymous/reactnativeexpo/MainApplication.kt +0 -57
  32. package/template-react-native/android/app/src/main/res/drawable/ic_launcher_background.xml +0 -6
  33. package/template-react-native/android/app/src/main/res/drawable/rn_edit_text_material.xml +0 -37
  34. package/template-react-native/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
  35. package/template-react-native/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
  36. package/template-react-native/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
  37. package/template-react-native/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
  38. package/template-react-native/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
  39. package/template-react-native/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -5
  40. package/template-react-native/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -5
  41. package/template-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  42. package/template-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
  43. package/template-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  44. package/template-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  45. package/template-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
  46. package/template-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  47. package/template-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  48. package/template-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
  49. package/template-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  50. package/template-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  51. package/template-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
  52. package/template-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  53. package/template-react-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  54. package/template-react-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
  55. package/template-react-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  56. package/template-react-native/android/app/src/main/res/values/colors.xml +0 -6
  57. package/template-react-native/android/app/src/main/res/values/strings.xml +0 -5
  58. package/template-react-native/android/app/src/main/res/values/styles.xml +0 -10
  59. package/template-react-native/android/app/src/main/res/values-night/colors.xml +0 -1
  60. package/template-react-native/android/build.gradle +0 -37
  61. package/template-react-native/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  62. package/template-react-native/android/gradle/wrapper/gradle-wrapper.properties +0 -7
  63. package/template-react-native/android/gradle.properties +0 -59
  64. package/template-react-native/android/gradlew +0 -251
  65. package/template-react-native/android/gradlew.bat +0 -94
  66. package/template-react-native/android/settings.gradle +0 -39
  67. package/template-react-native/ios/.xcode.env +0 -11
  68. package/template-react-native/ios/Podfile +0 -64
  69. package/template-react-native/ios/Podfile.lock +0 -2131
  70. package/template-react-native/ios/Podfile.properties.json +0 -5
  71. package/template-react-native/ios/_gitignore +0 -30
  72. package/template-react-native/ios/reactnativeexpo/AppDelegate.swift +0 -70
  73. package/template-react-native/ios/reactnativeexpo/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
  74. package/template-react-native/ios/reactnativeexpo/Images.xcassets/AppIcon.appiconset/Contents.json +0 -14
  75. package/template-react-native/ios/reactnativeexpo/Images.xcassets/Contents.json +0 -6
  76. package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenBackground.colorset/Contents.json +0 -20
  77. package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/Contents.json +0 -23
  78. package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/image.png +0 -0
  79. package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/image@2x.png +0 -0
  80. package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/image@3x.png +0 -0
  81. package/template-react-native/ios/reactnativeexpo/Info.plist +0 -74
  82. package/template-react-native/ios/reactnativeexpo/PrivacyInfo.xcprivacy +0 -48
  83. package/template-react-native/ios/reactnativeexpo/SplashScreen.storyboard +0 -44
  84. package/template-react-native/ios/reactnativeexpo/Supporting/Expo.plist +0 -12
  85. package/template-react-native/ios/reactnativeexpo/reactnativeexpo-Bridging-Header.h +0 -3
  86. package/template-react-native/ios/reactnativeexpo/reactnativeexpo.entitlements +0 -5
  87. package/template-react-native/ios/reactnativeexpo.xcodeproj/project.pbxproj +0 -545
  88. package/template-react-native/ios/reactnativeexpo.xcodeproj/xcshareddata/xcschemes/reactnativeexpo.xcscheme +0 -88
  89. package/template-react-native/ios/reactnativeexpo.xcworkspace/contents.xcworkspacedata +0 -10
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { useEvmAddress, useIsSignedIn } from "@coinbase/cdp-hooks";
3
+ import { useEvmAddress, useIsSignedIn, useSolanaAddress } from "@coinbase/cdp-hooks";
4
+ import { Connection, clusterApiUrl, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
4
5
  import { useCallback, useEffect, useMemo, useState, lazy, Suspense } from "react";
5
6
  import {
6
7
  createPublicClient,
@@ -16,8 +17,8 @@ import FundWallet from "@/components/FundWallet";
16
17
  import Header from "@/components/Header";
17
18
  import UserBalance from "@/components/UserBalance";
18
19
 
19
- // Dynamically determine component path (Onramp only supports EVM for now, Solana support will be added later)
20
- const getComponentPath = () => {
20
+ // Dynamically determine component path for EVM transactions
21
+ const getEVMComponentPath = () => {
21
22
  const isSmartAccount = process.env.NEXT_PUBLIC_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE === "smart";
22
23
 
23
24
  if (isSmartAccount) {
@@ -27,7 +28,8 @@ const getComponentPath = () => {
27
28
  }
28
29
  };
29
30
 
30
- const TransactionComponent = lazy(() => import(/* @vite-ignore */ getComponentPath()));
31
+ const EVMTransactionComponent = lazy(() => import(/* @vite-ignore */ getEVMComponentPath()));
32
+ const SolanaTransactionComponent = lazy(() => import("@/components/SolanaTransaction"));
31
33
 
32
34
  /**
33
35
  * Create a viem client to access user's balance on the Base network
@@ -45,7 +47,17 @@ const sepoliaClient = createPublicClient({
45
47
  transport: http(),
46
48
  });
47
49
 
48
- const useBalance = (
50
+ /**
51
+ * Create a Solana connection to access user's balance on Solana Mainnet
52
+ */
53
+ const solanaMainnetConnection = new Connection(clusterApiUrl("mainnet-beta"));
54
+
55
+ /**
56
+ * Create a Solana connection to access user's balance on Solana Devnet
57
+ */
58
+ const solanaDevnetConnection = new Connection(clusterApiUrl("devnet"));
59
+
60
+ const useEvmBalance = (
49
61
  address: Address | null,
50
62
  client: PublicClient<Transport, typeof base | typeof baseSepolia, undefined, undefined>,
51
63
  poll = false,
@@ -75,56 +87,152 @@ const useBalance = (
75
87
  return { balance, formattedBalance, getBalance };
76
88
  };
77
89
 
90
+ const useSolanaBalance = (address: string | null, connection: Connection, poll = false) => {
91
+ const [balance, setBalance] = useState<bigint | undefined>(undefined);
92
+
93
+ const formattedBalance = useMemo(() => {
94
+ if (balance === undefined) return undefined;
95
+ // Convert lamports to SOL
96
+ return formatSol(Number(balance));
97
+ }, [balance]);
98
+
99
+ const getBalance = useCallback(async () => {
100
+ if (!address) return;
101
+ try {
102
+ const lamports = await connection.getBalance(new PublicKey(address));
103
+ setBalance(BigInt(lamports));
104
+ } catch (error) {
105
+ console.error("Error fetching Solana balance:", error);
106
+ setBalance(BigInt(0));
107
+ }
108
+ }, [address, connection]);
109
+
110
+ useEffect(() => {
111
+ if (!poll) {
112
+ return;
113
+ }
114
+ getBalance();
115
+ const interval = setInterval(getBalance, 500);
116
+ return () => clearInterval(interval);
117
+ }, [getBalance, poll]);
118
+
119
+ return { balance, formattedBalance, getBalance };
120
+ };
121
+
122
+ /**
123
+ * Format a Solana balance.
124
+ *
125
+ * @param lamports - The balance in lamports.
126
+ * @returns The formatted balance.
127
+ */
128
+ function formatSol(lamports: number) {
129
+ const maxDecimalPlaces = 9;
130
+ const roundedStr = (lamports / LAMPORTS_PER_SOL).toFixed(maxDecimalPlaces);
131
+ return roundedStr.replace(/0+$/, "").replace(/\.$/, "");
132
+ }
133
+
78
134
  /**
79
- * The Signed In screen
135
+ * The Signed In screen with onramp support for both EVM and Solana
80
136
  */
81
137
  export default function SignedInScreen() {
82
138
  const { isSignedIn } = useIsSignedIn();
83
139
  const { evmAddress } = useEvmAddress();
140
+ const { solanaAddress } = useSolanaAddress();
84
141
 
85
- // Onramp only supports EVM accounts
142
+ const { formattedBalance, getBalance } = useEvmBalance(evmAddress, client, true);
143
+ const { formattedBalance: formattedBalanceSepolia, getBalance: getBalanceSepolia } =
144
+ useEvmBalance(evmAddress, sepoliaClient, true);
86
145
 
87
- const { formattedBalance, getBalance } = useBalance(evmAddress, client, true);
88
- const { formattedBalance: formattedBalanceSepolia, getBalance: getBalanceSepolia } = useBalance(
89
- evmAddress,
90
- sepoliaClient,
91
- true,
92
- );
146
+ const { formattedBalance: formattedBalanceSolana, getBalance: getBalanceSolana } =
147
+ useSolanaBalance(solanaAddress, solanaMainnetConnection);
148
+ const { formattedBalance: formattedBalanceSolanaDevnet, getBalance: getBalanceSolanaDevnet } =
149
+ useSolanaBalance(solanaAddress, solanaDevnetConnection, true);
93
150
 
94
151
  return (
95
152
  <>
96
153
  <Header />
97
154
  <main className="main flex-col-container flex-grow">
98
- <p className="page-heading">Fund your wallet on Base</p>
99
- <div className="main-inner flex-col-container">
100
- <div className="card card--user-balance">
101
- <UserBalance balance={formattedBalance} />
102
- </div>
103
- <div className="card card--transaction">
104
- {isSignedIn && evmAddress && <FundWallet onSuccess={getBalance} />}
105
- </div>
106
- </div>
107
- <hr className="page-divider" />
108
- <p className="page-heading">Send a transaction on Base Sepolia</p>
109
- <div className="main-inner flex-col-container">
110
- <div className="card card--user-balance">
111
- <UserBalance
112
- balance={formattedBalanceSepolia}
113
- faucetName="Base Sepolia Faucet"
114
- faucetUrl="https://portal.cdp.coinbase.com/products/faucet"
115
- />
116
- </div>
117
- <div className="card card--transaction">
118
- {isSignedIn && evmAddress && (
119
- <Suspense fallback={<div>Loading transaction component...</div>}>
120
- <TransactionComponent
155
+ {evmAddress && (
156
+ <>
157
+ <p className="page-heading">Fund your EVM wallet on Base</p>
158
+ <div className="main-inner flex-col-container">
159
+ <div className="card card--user-balance">
160
+ <UserBalance balance={formattedBalance} />
161
+ </div>
162
+ <div className="card card--transaction">
163
+ {isSignedIn && (
164
+ <FundWallet
165
+ onSuccess={getBalance}
166
+ network="base"
167
+ cryptoCurrency="eth"
168
+ destinationAddress={evmAddress}
169
+ />
170
+ )}
171
+ </div>
172
+ </div>
173
+ <hr className="page-divider" />
174
+ <p className="page-heading">Send an EVM transaction on Base Sepolia</p>
175
+ <div className="main-inner flex-col-container">
176
+ <div className="card card--user-balance">
177
+ <UserBalance
121
178
  balance={formattedBalanceSepolia}
122
- onSuccess={getBalanceSepolia}
179
+ faucetName="Base Sepolia Faucet"
180
+ faucetUrl="https://portal.cdp.coinbase.com/products/faucet"
181
+ />
182
+ </div>
183
+ <div className="card card--transaction">
184
+ {isSignedIn && (
185
+ <Suspense fallback={<div>Loading transaction component...</div>}>
186
+ <EVMTransactionComponent
187
+ balance={formattedBalanceSepolia}
188
+ onSuccess={getBalanceSepolia}
189
+ />
190
+ </Suspense>
191
+ )}
192
+ </div>
193
+ </div>
194
+ </>
195
+ )}
196
+
197
+ {solanaAddress && (
198
+ <>
199
+ {evmAddress && <hr className="page-divider" />}
200
+ <p className="page-heading">Fund your Solana wallet on Mainnet</p>
201
+ <div className="main-inner flex-col-container">
202
+ <div className="card card--user-balance">
203
+ <UserBalance balance={formattedBalanceSolana} />
204
+ </div>
205
+ <div className="card card--transaction">
206
+ {isSignedIn && (
207
+ <FundWallet
208
+ onSuccess={getBalanceSolana}
209
+ network="solana"
210
+ cryptoCurrency="sol"
211
+ destinationAddress={solanaAddress}
212
+ />
213
+ )}
214
+ </div>
215
+ </div>
216
+ <hr className="page-divider" />
217
+ <p className="page-heading">Send a Solana transaction on Devnet</p>
218
+ <div className="main-inner flex-col-container">
219
+ <div className="card card--user-balance">
220
+ <UserBalance
221
+ balance={formattedBalanceSolanaDevnet}
222
+ faucetName="Solana Devnet Faucet"
223
+ faucetUrl="https://portal.cdp.coinbase.com/products/faucet?network=solana-devnet"
123
224
  />
124
- </Suspense>
125
- )}
126
- </div>
127
- </div>
225
+ </div>
226
+ <div className="card card--transaction">
227
+ {isSignedIn && (
228
+ <Suspense fallback={<div>Loading transaction component...</div>}>
229
+ <SolanaTransactionComponent onSuccess={getBalanceSolanaDevnet} />
230
+ </Suspense>
231
+ )}
232
+ </div>
233
+ </div>
234
+ </>
235
+ )}
128
236
  </main>
129
237
  </>
130
238
  );
@@ -2,126 +2,136 @@
2
2
 
3
3
  import { Buffer } from "buffer";
4
4
 
5
- import { useSolanaAddress, useSignSolanaTransaction } from "@coinbase/cdp-hooks";
5
+ import { useSolanaAddress } from "@coinbase/cdp-hooks";
6
+ import {
7
+ SendSolanaTransactionButton,
8
+ type SendSolanaTransactionButtonProps,
9
+ } from "@coinbase/cdp-react/components/SendSolanaTransactionButton";
6
10
  import { Button } from "@coinbase/cdp-react/components/ui/Button";
11
+ import { LoadingSkeleton } from "@coinbase/cdp-react/components/ui/LoadingSkeleton";
7
12
  import {
8
13
  PublicKey,
9
14
  Transaction,
10
15
  SystemProgram,
11
16
  SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
12
17
  } from "@solana/web3.js";
13
- import { useEffect, useState } from "react";
14
-
15
- import { IconCheck, IconCopy } from "./Icons";
18
+ import { useMemo, useState } from "react";
16
19
 
17
20
  interface Props {
21
+ balance?: string;
18
22
  onSuccess?: () => void;
19
23
  }
20
24
 
21
25
  /**
22
- * Solana transaction component that demonstrates signing transactions.
26
+ * This component demonstrates how to send a Solana transaction.
23
27
  *
24
- * @param props - The component props
28
+ * @param {Props} props - The props for the SolanaTransaction component.
29
+ * @param {string} [props.balance] - The user's balance.
30
+ * @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
31
+ * @returns A component that displays a transaction form and a transaction signature.
25
32
  */
26
33
  export default function SolanaTransaction(props: Props) {
27
- const { onSuccess } = props;
34
+ const { balance, onSuccess } = props;
28
35
  const { solanaAddress } = useSolanaAddress();
36
+ const [transactionSignature, setTransactionSignature] = useState("");
29
37
  const [error, setError] = useState("");
30
- const [isLoading, setIsLoading] = useState(false);
31
- const [signedTransaction, setSignedTransaction] = useState<string | null>(null);
32
- const { signSolanaTransaction } = useSignSolanaTransaction();
33
- const [isCopied, setIsCopied] = useState(false);
34
-
35
- const handleSignTransaction = async () => {
36
- if (!solanaAddress) {
37
- alert("No Solana address available.");
38
- return;
39
- }
40
-
41
- setIsLoading(true);
42
- setError("");
43
- setSignedTransaction(null);
44
-
45
- try {
46
- const transaction = createAndEncodeTransaction(solanaAddress);
47
- const result = await signSolanaTransaction({
48
- solanaAccount: solanaAddress,
49
- transaction,
50
- });
51
-
52
- setSignedTransaction(result.signedTransaction);
53
- onSuccess?.();
54
- } catch (err) {
55
- const errorMessage = err instanceof Error ? err.message : "Transaction signing failed";
56
- setError(errorMessage);
57
- } finally {
58
- setIsLoading(false);
59
- }
38
+
39
+ const hasBalance = useMemo(() => {
40
+ return balance && balance !== "0";
41
+ }, [balance]);
42
+
43
+ const transaction = useMemo(() => {
44
+ if (!solanaAddress) return "";
45
+ return createAndEncodeTransaction(solanaAddress);
46
+ }, [solanaAddress]);
47
+
48
+ const handleTransactionError: SendSolanaTransactionButtonProps["onError"] = error => {
49
+ setTransactionSignature("");
50
+ setError(error.message);
60
51
  };
61
52
 
62
- const copyTransaction = async () => {
63
- if (!signedTransaction) return;
64
- try {
65
- await navigator.clipboard.writeText(signedTransaction);
66
- setIsCopied(true);
67
- } catch (error) {
68
- console.error(error);
69
- }
53
+ const handleTransactionSuccess: SendSolanaTransactionButtonProps["onSuccess"] = signature => {
54
+ setTransactionSignature(signature);
55
+ setError("");
56
+ onSuccess?.();
70
57
  };
71
58
 
72
- useEffect(() => {
73
- if (!isCopied) return;
74
- const timeout = setTimeout(() => {
75
- setIsCopied(false);
76
- }, 2000);
77
- return () => clearTimeout(timeout);
78
- }, [isCopied]);
59
+ const handleReset = () => {
60
+ setTransactionSignature("");
61
+ setError("");
62
+ };
79
63
 
80
64
  return (
81
- <div className="transaction-container">
82
- <div className="transaction-section">
83
- <h3>Sign Solana Transaction</h3>
84
- <p className="transaction-description">
85
- This example demonstrates signing a Solana transaction with your embedded wallet.
86
- </p>
87
-
88
- <Button
89
- className="tx-button"
90
- onClick={handleSignTransaction}
91
- variant="secondary"
92
- disabled={!solanaAddress || isLoading}
93
- >
94
- {isLoading ? "Signing..." : "Sign Transaction"}
95
- </Button>
96
-
97
- {error && (
98
- <div className="error-container">
99
- <p className="error-text">{error}</p>
100
- </div>
101
- )}
102
-
103
- {signedTransaction && (
104
- <div className="success-container">
105
- <h4>Transaction Signed Successfully</h4>
106
- <div className="transaction-result">
107
- <label>Signed Transaction:</label>
108
- <button
109
- aria-label="copy signed transaction"
110
- className="flex-row-container copy-address-button"
111
- onClick={copyTransaction}
112
- >
113
- {!isCopied && <IconCopy className="user-icon user-icon--copy" />}
114
- {isCopied && <IconCheck className="user-icon user-icon--check" />}
115
- <span className="wallet-address">
116
- {signedTransaction.slice(0, 4)}...{signedTransaction.slice(-4)}
117
- </span>
118
- </button>
119
- <small>Click to copy signed transaction</small>
120
- </div>
121
- </div>
122
- )}
123
- </div>
124
- </div>
65
+ <>
66
+ {balance === undefined && (
67
+ <>
68
+ <h2 className="card-title">Send a Solana transaction</h2>
69
+ <LoadingSkeleton className="loading--text" />
70
+ <LoadingSkeleton className="loading--btn" />
71
+ </>
72
+ )}
73
+ {balance !== undefined && (
74
+ <>
75
+ {!transactionSignature && error && (
76
+ <>
77
+ <h2 className="card-title">Oops</h2>
78
+ <p>{error}</p>
79
+ <Button className="tx-button" onClick={handleReset} variant="secondary">
80
+ Reset and try again
81
+ </Button>
82
+ </>
83
+ )}
84
+ {!transactionSignature && !error && (
85
+ <>
86
+ <h2 className="card-title">Send a Solana transaction</h2>
87
+ {hasBalance && solanaAddress && (
88
+ <>
89
+ <p>Send 1 Lamport to yourself on Solana Devnet</p>
90
+ <SendSolanaTransactionButton
91
+ account={solanaAddress}
92
+ network="solana-devnet"
93
+ transaction={transaction}
94
+ onError={handleTransactionError}
95
+ onSuccess={handleTransactionSuccess}
96
+ />
97
+ </>
98
+ )}
99
+ {!hasBalance && (
100
+ <>
101
+ <p>
102
+ This example transaction sends a tiny amount of SOL from your wallet to itself.
103
+ </p>
104
+ <p>
105
+ Get some from{" "}
106
+ <a href="https://faucet.solana.com/" target="_blank" rel="noopener noreferrer">
107
+ Solana Devnet Faucet
108
+ </a>
109
+ </p>
110
+ </>
111
+ )}
112
+ </>
113
+ )}
114
+ {transactionSignature && (
115
+ <>
116
+ <h2 className="card-title">Transaction sent</h2>
117
+ <p>
118
+ Transaction signature:{" "}
119
+ <a
120
+ href={`https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`}
121
+ target="_blank"
122
+ rel="noopener noreferrer"
123
+ >
124
+ {transactionSignature.slice(0, 4)}...{transactionSignature.slice(-4)}
125
+ </a>
126
+ </p>
127
+ <Button variant="secondary" className="tx-button" onClick={handleReset}>
128
+ Send another transaction
129
+ </Button>
130
+ </>
131
+ )}
132
+ </>
133
+ )}
134
+ </>
125
135
  );
126
136
  }
127
137
 
@@ -24,10 +24,12 @@ export const getBuyOptions: FetchBuyOptions = async params => {
24
24
 
25
25
  if (!response.ok) {
26
26
  const errorData = await response.json();
27
+ console.error("getBuyOptions: API error:", errorData);
27
28
  throw new Error(errorData.error || "Failed to fetch buy options");
28
29
  }
29
30
 
30
- return await response.json();
31
+ const result = await response.json();
32
+ return result;
31
33
  } catch (error) {
32
34
  console.error("Error fetching buy options:", error);
33
35
  throw error;
@@ -52,10 +54,12 @@ export const createBuyQuote: FetchBuyQuote = async request => {
52
54
 
53
55
  if (!response.ok) {
54
56
  const errorData = await response.json();
57
+ console.error("createBuyQuote: API error:", errorData);
55
58
  throw new Error(errorData.error || "Failed to create buy quote");
56
59
  }
57
60
 
58
- return await response.json();
61
+ const result = await response.json();
62
+ return result;
59
63
  } catch (error) {
60
64
  console.error("Error creating buy quote:", error);
61
65
  throw error;
@@ -1,8 +1,8 @@
1
1
  import { useEvmAddress } from "@coinbase/cdp-hooks";
2
2
  import {
3
- SendTransactionButton,
4
- type SendTransactionButtonProps,
5
- } from "@coinbase/cdp-react/components/SendTransactionButton";
3
+ SendEvmTransactionButton,
4
+ type SendEvmTransactionButtonProps,
5
+ } from "@coinbase/cdp-react/components/SendEvmTransactionButton";
6
6
  import { Button } from "@coinbase/cdp-react/components/ui/Button";
7
7
  import { LoadingSkeleton } from "@coinbase/cdp-react/components/ui/LoadingSkeleton";
8
8
  import { useMemo, useState } from "react";
@@ -30,7 +30,7 @@ function EOATransaction(props: Props) {
30
30
  return balance && balance !== "0";
31
31
  }, [balance]);
32
32
 
33
- const transaction = useMemo<SendTransactionButtonProps["transaction"]>(() => {
33
+ const transaction = useMemo<SendEvmTransactionButtonProps["transaction"]>(() => {
34
34
  return {
35
35
  to: evmAddress, // Send to yourself for testing
36
36
  value: 1000000000000n, // 0.000001 ETH in wei
@@ -40,12 +40,12 @@ function EOATransaction(props: Props) {
40
40
  };
41
41
  }, [evmAddress]);
42
42
 
43
- const handleTransactionError: SendTransactionButtonProps["onError"] = error => {
43
+ const handleTransactionError: SendEvmTransactionButtonProps["onError"] = error => {
44
44
  setTransactionHash("");
45
45
  setError(error.message);
46
46
  };
47
47
 
48
- const handleTransactionSuccess: SendTransactionButtonProps["onSuccess"] = hash => {
48
+ const handleTransactionSuccess: SendEvmTransactionButtonProps["onSuccess"] = hash => {
49
49
  setTransactionHash(hash);
50
50
  setError("");
51
51
  onSuccess?.();
@@ -82,7 +82,7 @@ function EOATransaction(props: Props) {
82
82
  {hasBalance && evmAddress && (
83
83
  <>
84
84
  <p>Send 0.000001 ETH to yourself on Base Sepolia</p>
85
- <SendTransactionButton
85
+ <SendEvmTransactionButton
86
86
  account={evmAddress}
87
87
  network="base-sepolia"
88
88
  transaction={transaction}
@@ -47,7 +47,6 @@ function SignedInScreen() {
47
47
  const { solanaAddress } = useSolanaAddress();
48
48
  const [balance, setBalance] = useState<bigint | undefined>(undefined);
49
49
 
50
- const isSolana = !!CDP_CONFIG.solana;
51
50
  const address = isSolana ? solanaAddress : evmAddress;
52
51
 
53
52
  const formattedBalance = useMemo(() => {
@@ -59,7 +58,7 @@ function SignedInScreen() {
59
58
  // Convert wei to ETH
60
59
  return formatEther(balance);
61
60
  }
62
- }, [balance, isSolana]);
61
+ }, [balance]);
63
62
 
64
63
  const getBalance = useCallback(async () => {
65
64
  if (isSolana && solanaAddress) {
@@ -73,7 +72,7 @@ function SignedInScreen() {
73
72
  });
74
73
  setBalance(weiBalance);
75
74
  }
76
- }, [evmAddress, solanaAddress, isSolana]);
75
+ }, [evmAddress, solanaAddress]);
77
76
 
78
77
  useEffect(() => {
79
78
  getBalance();