@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,126 +1,135 @@
1
1
  import { Buffer } from "buffer";
2
2
 
3
- import { useSolanaAddress, useSignSolanaTransaction } from "@coinbase/cdp-hooks";
3
+ import { useSolanaAddress } from "@coinbase/cdp-hooks";
4
+ import {
5
+ SendSolanaTransactionButton,
6
+ type SendSolanaTransactionButtonProps,
7
+ } from "@coinbase/cdp-react/components/SendSolanaTransactionButton";
4
8
  import { Button } from "@coinbase/cdp-react/components/ui/Button";
9
+ import { LoadingSkeleton } from "@coinbase/cdp-react/components/ui/LoadingSkeleton";
5
10
  import {
6
11
  PublicKey,
7
12
  Transaction,
8
13
  SystemProgram,
9
14
  SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
10
15
  } from "@solana/web3.js";
11
- import { useEffect, useState } from "react";
12
-
13
- import { IconCheck, IconCopy } from "./Icons";
16
+ import { useMemo, useState } from "react";
14
17
 
15
18
  interface Props {
19
+ balance?: string;
16
20
  onSuccess?: () => void;
17
21
  }
18
22
 
19
23
  /**
20
- * Solana transaction component that demonstrates signing transactions.
24
+ * This component demonstrates how to send a Solana transaction.
21
25
  *
22
- * @param props - The component props
26
+ * @param {Props} props - The props for the SolanaTransaction component.
27
+ * @param {string} [props.balance] - The user's balance.
28
+ * @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
29
+ * @returns A component that displays a transaction form and a transaction signature.
23
30
  */
24
31
  function SolanaTransaction(props: Props) {
25
- const { onSuccess } = props;
32
+ const { balance, onSuccess } = props;
26
33
  const { solanaAddress } = useSolanaAddress();
34
+ const [transactionSignature, setTransactionSignature] = useState("");
27
35
  const [error, setError] = useState("");
28
- const [isLoading, setIsLoading] = useState(false);
29
- const [signedTransaction, setSignedTransaction] = useState<string | null>(null);
30
- const { signSolanaTransaction } = useSignSolanaTransaction();
31
- const [isCopied, setIsCopied] = useState(false);
32
-
33
- const handleSignTransaction = async () => {
34
- if (!solanaAddress) {
35
- alert("No Solana address available.");
36
- return;
37
- }
38
-
39
- setIsLoading(true);
40
- setError("");
41
- setSignedTransaction(null);
42
-
43
- try {
44
- const transaction = createAndEncodeTransaction(solanaAddress);
45
- const result = await signSolanaTransaction({
46
- solanaAccount: solanaAddress,
47
- transaction,
48
- });
49
-
50
- setSignedTransaction(result.signedTransaction);
51
- onSuccess?.();
52
- } catch (err) {
53
- const errorMessage = err instanceof Error ? err.message : "Transaction signing failed";
54
- setError(errorMessage);
55
- alert(`Transaction Failed: ${errorMessage}`);
56
- } finally {
57
- setIsLoading(false);
58
- }
36
+
37
+ const hasBalance = useMemo(() => {
38
+ return balance && balance !== "0";
39
+ }, [balance]);
40
+
41
+ const transaction = useMemo(() => {
42
+ if (!solanaAddress) return "";
43
+ return createAndEncodeTransaction(solanaAddress);
44
+ }, [solanaAddress]);
45
+
46
+ const handleTransactionError: SendSolanaTransactionButtonProps["onError"] = error => {
47
+ setTransactionSignature("");
48
+ setError(error.message);
59
49
  };
60
50
 
61
- const copyTransaction = async () => {
62
- if (!signedTransaction) return;
63
- try {
64
- await navigator.clipboard.writeText(signedTransaction);
65
- setIsCopied(true);
66
- } catch (error) {
67
- console.error(error);
68
- }
51
+ const handleTransactionSuccess: SendSolanaTransactionButtonProps["onSuccess"] = signature => {
52
+ setTransactionSignature(signature);
53
+ setError("");
54
+ onSuccess?.();
69
55
  };
70
56
 
71
- useEffect(() => {
72
- if (!isCopied) return;
73
- const timeout = setTimeout(() => {
74
- setIsCopied(false);
75
- }, 2000);
76
- return () => clearTimeout(timeout);
77
- }, [isCopied]);
57
+ const handleReset = () => {
58
+ setTransactionSignature("");
59
+ setError("");
60
+ };
78
61
 
79
62
  return (
80
- <div className="transaction-container">
81
- <div className="transaction-section">
82
- <h3>Sign Solana Transaction</h3>
83
- <p className="transaction-description">
84
- This example demonstrates signing a Solana transaction with your embedded wallet.
85
- </p>
86
-
87
- <Button
88
- className="tx-button"
89
- onClick={handleSignTransaction}
90
- variant="secondary"
91
- disabled={!solanaAddress || isLoading}
92
- >
93
- {isLoading ? "Signing..." : "Sign Transaction"}
94
- </Button>
95
-
96
- {error && (
97
- <div className="error-container">
98
- <p className="error-text">{error}</p>
99
- </div>
100
- )}
101
-
102
- {signedTransaction && (
103
- <div className="success-container">
104
- <h4>Transaction Signed Successfully</h4>
105
- <div className="transaction-result">
106
- <label>Signed Transaction:</label>
107
- <Button
108
- aria-label="copy signed transaction"
109
- className="flex-row-container copy-address-button"
110
- onClick={copyTransaction}
111
- >
112
- {!isCopied && <IconCopy className="user-icon user-icon--copy" />}
113
- {isCopied && <IconCheck className="user-icon user-icon--check" />}
114
- <span className="wallet-address">
115
- {signedTransaction.slice(0, 4)}...{signedTransaction.slice(-4)}
116
- </span>
63
+ <>
64
+ {balance === undefined && (
65
+ <>
66
+ <h2 className="card-title">Send a Solana transaction</h2>
67
+ <LoadingSkeleton className="loading--text" />
68
+ <LoadingSkeleton className="loading--btn" />
69
+ </>
70
+ )}
71
+ {balance !== undefined && (
72
+ <>
73
+ {!transactionSignature && error && (
74
+ <>
75
+ <h2 className="card-title">Oops</h2>
76
+ <p>{error}</p>
77
+ <Button className="tx-button" onClick={handleReset} variant="secondary">
78
+ Reset and try again
79
+ </Button>
80
+ </>
81
+ )}
82
+ {!transactionSignature && !error && (
83
+ <>
84
+ <h2 className="card-title">Send a Solana transaction</h2>
85
+ {hasBalance && solanaAddress && (
86
+ <>
87
+ <p>Send 1 Lamport to yourself on Solana Devnet</p>
88
+ <SendSolanaTransactionButton
89
+ account={solanaAddress}
90
+ network="solana-devnet"
91
+ transaction={transaction}
92
+ onError={handleTransactionError}
93
+ onSuccess={handleTransactionSuccess}
94
+ />
95
+ </>
96
+ )}
97
+ {!hasBalance && (
98
+ <>
99
+ <p>
100
+ This example transaction sends a tiny amount of SOL from your wallet to itself.
101
+ </p>
102
+ <p>
103
+ Get some from{" "}
104
+ <a href="https://faucet.solana.com/" target="_blank" rel="noopener noreferrer">
105
+ Solana Devnet Faucet
106
+ </a>
107
+ </p>
108
+ </>
109
+ )}
110
+ </>
111
+ )}
112
+ {transactionSignature && (
113
+ <>
114
+ <h2 className="card-title">Transaction sent</h2>
115
+ <p>
116
+ Transaction signature:{" "}
117
+ <a
118
+ href={`https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`}
119
+ target="_blank"
120
+ rel="noopener noreferrer"
121
+ >
122
+ {transactionSignature.slice(0, 6)}...{transactionSignature.slice(-4)}
123
+ </a>
124
+ </p>
125
+ <Button variant="secondary" className="tx-button" onClick={handleReset}>
126
+ Send another transaction
117
127
  </Button>
118
- <small>Click to copy signed transaction</small>
119
- </div>
120
- </div>
121
- )}
122
- </div>
123
- </div>
128
+ </>
129
+ )}
130
+ </>
131
+ )}
132
+ </>
124
133
  );
125
134
  }
126
135
 
@@ -20,15 +20,37 @@ import { DarkModeToggle } from "./components/DarkModeToggle";
20
20
  import { WalletHeader } from "./components/WalletHeader";
21
21
  import { AuthMethod } from "./types";
22
22
 
23
- const cdpConfig: Config = {
23
+ const ethereumAccountType = process.env.EXPO_PUBLIC_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE
24
+ ? process.env.EXPO_PUBLIC_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE === "smart"
25
+ ? "smart"
26
+ : "eoa"
27
+ : undefined;
28
+
29
+ const solanaAccountType = process.env.EXPO_PUBLIC_CDP_CREATE_SOLANA_ACCOUNT
30
+ ? process.env.EXPO_PUBLIC_CDP_CREATE_SOLANA_ACCOUNT === "true"
31
+ : undefined;
32
+
33
+ if (!ethereumAccountType && !solanaAccountType) {
34
+ throw new Error(
35
+ "Either EXPO_PUBLIC_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE or EXPO_PUBLIC_CDP_CREATE_SOLANA_ACCOUNT must be defined",
36
+ );
37
+ }
38
+
39
+ const cdpConfig = {
24
40
  projectId: process.env.EXPO_PUBLIC_CDP_PROJECT_ID,
25
41
  basePath: process.env.EXPO_PUBLIC_CDP_BASE_PATH,
26
- ethereum: {
27
- createOnLogin:
28
- process.env.EXPO_PUBLIC_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE === "smart" ? "smart" : "eoa",
29
- },
42
+ ...(ethereumAccountType && {
43
+ ethereum: {
44
+ createOnLogin: ethereumAccountType,
45
+ },
46
+ }),
47
+ ...(solanaAccountType && {
48
+ solana: {
49
+ createOnLogin: solanaAccountType,
50
+ },
51
+ }),
30
52
  useMock: process.env.EXPO_PUBLIC_CDP_USE_MOCK === "true",
31
- };
53
+ } as Config;
32
54
 
33
55
  /**
34
56
  * A multi-step authentication component that handles email and SMS-based sign-in flows.
@@ -0,0 +1,368 @@
1
+ import { Buffer } from "buffer";
2
+
3
+ import { useSolanaAddress, useSendSolanaTransaction } from "@coinbase/cdp-hooks";
4
+ import * as Clipboard from "expo-clipboard";
5
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
6
+ import {
7
+ View,
8
+ Text,
9
+ TouchableOpacity,
10
+ StyleSheet,
11
+ Alert,
12
+ ActivityIndicator,
13
+ Linking,
14
+ } from "react-native";
15
+ import {
16
+ PublicKey,
17
+ Transaction,
18
+ SystemProgram,
19
+ SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
20
+ Connection,
21
+ clusterApiUrl,
22
+ LAMPORTS_PER_SOL,
23
+ } from "@solana/web3.js";
24
+ import { useTheme } from "./theme/ThemeContext";
25
+
26
+ interface Props {
27
+ onSuccess?: () => void;
28
+ }
29
+
30
+ const SOLANA_FAUCET_ADDRESS = "EeVPcnRE1mhcY85wAh3uPJG1uFiTNya9dCJjNUPABXzo";
31
+
32
+ const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
33
+
34
+ function SolanaTransaction(props: Props) {
35
+ const { onSuccess } = props;
36
+ const { colors } = useTheme();
37
+ const { solanaAddress } = useSolanaAddress();
38
+ const [balance, setBalance] = useState<number | undefined>(undefined);
39
+ const [error, setError] = useState("");
40
+ const [isLoading, setIsLoading] = useState(false);
41
+ const { sendSolanaTransaction: sendTransaction } = useSendSolanaTransaction();
42
+ const [transactionSignature, setTransactionSignature] = useState<string | undefined>(undefined);
43
+
44
+ const formattedBalance = useMemo(() => {
45
+ if (balance === undefined) return undefined;
46
+ return (balance / LAMPORTS_PER_SOL).toFixed(6);
47
+ }, [balance]);
48
+
49
+ const hasBalance = useMemo(() => {
50
+ return formattedBalance && parseFloat(formattedBalance) > 0;
51
+ }, [formattedBalance]);
52
+
53
+ const getBalance = useCallback(async () => {
54
+ if (!solanaAddress) return;
55
+ try {
56
+ const balance = await connection.getBalance(new PublicKey(solanaAddress));
57
+ setBalance(balance);
58
+ } catch (error) {
59
+ console.error("Failed to fetch balance:", error);
60
+ }
61
+ }, [solanaAddress]);
62
+
63
+ useEffect(() => {
64
+ getBalance();
65
+ const interval = setInterval(getBalance, 5000);
66
+ return () => clearInterval(interval);
67
+ }, [getBalance]);
68
+
69
+ const transaction = useMemo(() => {
70
+ if (!solanaAddress) return "";
71
+ return createAndEncodeTransaction(solanaAddress);
72
+ }, [solanaAddress]);
73
+
74
+ const handleSendTransaction = async () => {
75
+ if (!solanaAddress) {
76
+ Alert.alert("Error", "No Solana address available.");
77
+ return;
78
+ }
79
+
80
+ setIsLoading(true);
81
+ setError("");
82
+ setTransactionSignature(undefined);
83
+
84
+ try {
85
+ const result = await sendTransaction({
86
+ network: "solana-devnet",
87
+ solanaAccount: solanaAddress,
88
+ transaction,
89
+ });
90
+
91
+ onSuccess?.();
92
+ setTransactionSignature(result.transactionSignature);
93
+ getBalance();
94
+ } catch (err) {
95
+ const errorMessage = err instanceof Error ? err.message : "Transaction failed";
96
+ setError(errorMessage);
97
+ Alert.alert("Transaction Failed", errorMessage + (errorMessage.endsWith(".") ? "" : "."));
98
+ } finally {
99
+ setIsLoading(false);
100
+ }
101
+ };
102
+
103
+ const copyToClipboard = async (text: string, label: string) => {
104
+ try {
105
+ await Clipboard.setStringAsync(text);
106
+ Alert.alert("Copied!", `${label} copied to clipboard.`);
107
+ } catch (error) {
108
+ Alert.alert("Error", "Failed to copy to clipboard.");
109
+ }
110
+ };
111
+
112
+ const openFaucet = () => {
113
+ const solanaFaucetUrl = `https://portal.cdp.coinbase.com/products/faucet?network=solana-devnet&token=SOL&address=${solanaAddress}`;
114
+ Alert.alert(
115
+ "Get testnet SOL",
116
+ "",
117
+ [
118
+ {
119
+ text: "Copy SOL Faucet Link",
120
+ onPress: () => copyToClipboard(solanaFaucetUrl, "SOL Faucet Link"),
121
+ },
122
+ {
123
+ text: "Open SOL Faucet",
124
+ onPress: () => Linking.openURL(solanaFaucetUrl),
125
+ style: "default",
126
+ },
127
+ { text: "Cancel", style: "cancel" },
128
+ ],
129
+ { cancelable: true },
130
+ );
131
+ };
132
+
133
+ const createStyles = () =>
134
+ StyleSheet.create({
135
+ container: {
136
+ flex: 1,
137
+ paddingHorizontal: 20,
138
+ paddingVertical: 20,
139
+ },
140
+ balanceSection: {
141
+ backgroundColor: colors.cardBackground,
142
+ borderRadius: 12,
143
+ padding: 24,
144
+ marginBottom: 16,
145
+ alignItems: "center",
146
+ borderWidth: 1,
147
+ borderColor: colors.border,
148
+ },
149
+ balanceTitle: {
150
+ fontSize: 16,
151
+ fontWeight: "500",
152
+ color: colors.textSecondary,
153
+ marginBottom: 8,
154
+ },
155
+ balanceAmount: {
156
+ fontSize: 32,
157
+ fontWeight: "bold",
158
+ color: colors.text,
159
+ marginBottom: 16,
160
+ },
161
+ faucetButton: {
162
+ backgroundColor: colors.accent,
163
+ paddingHorizontal: 20,
164
+ paddingVertical: 12,
165
+ borderRadius: 8,
166
+ alignItems: "center",
167
+ width: "100%",
168
+ },
169
+ faucetButtonText: {
170
+ color: "#ffffff",
171
+ fontSize: 16,
172
+ fontWeight: "600",
173
+ },
174
+ transactionSection: {
175
+ backgroundColor: colors.cardBackground,
176
+ borderRadius: 12,
177
+ padding: 20,
178
+ borderWidth: 1,
179
+ borderColor: colors.border,
180
+ },
181
+ sectionTitle: {
182
+ fontSize: 20,
183
+ fontWeight: "600",
184
+ color: colors.text,
185
+ marginBottom: 8,
186
+ },
187
+ sectionDescription: {
188
+ fontSize: 14,
189
+ color: colors.textSecondary,
190
+ marginBottom: 20,
191
+ lineHeight: 20,
192
+ },
193
+ sendButton: {
194
+ backgroundColor: colors.accent,
195
+ paddingVertical: 16,
196
+ borderRadius: 8,
197
+ alignItems: "center",
198
+ marginBottom: 16,
199
+ },
200
+ sendButtonDisabled: {
201
+ opacity: 0.6,
202
+ },
203
+ sendButtonText: {
204
+ color: "#ffffff",
205
+ fontSize: 16,
206
+ fontWeight: "600",
207
+ },
208
+ errorContainer: {
209
+ backgroundColor: "rgba(255, 0, 0, 0.1)",
210
+ padding: 12,
211
+ borderRadius: 8,
212
+ borderColor: "rgba(255, 0, 0, 0.3)",
213
+ borderWidth: 1,
214
+ marginBottom: 16,
215
+ },
216
+ errorText: {
217
+ color: "#cc0000",
218
+ fontSize: 14,
219
+ },
220
+ successContainer: {
221
+ backgroundColor: colors.accent + "20",
222
+ padding: 16,
223
+ borderRadius: 8,
224
+ borderColor: colors.accent + "50",
225
+ borderWidth: 1,
226
+ marginBottom: 16,
227
+ },
228
+ successTitle: {
229
+ color: colors.accent,
230
+ fontSize: 16,
231
+ fontWeight: "600",
232
+ marginBottom: 12,
233
+ },
234
+ hashContainer: {
235
+ marginBottom: 12,
236
+ },
237
+ hashLabel: {
238
+ color: colors.accent,
239
+ fontSize: 14,
240
+ fontWeight: "600",
241
+ marginBottom: 6,
242
+ },
243
+ hashButton: {
244
+ backgroundColor: colors.accent + "20",
245
+ borderRadius: 6,
246
+ padding: 12,
247
+ borderWidth: 1,
248
+ borderColor: colors.accent + "50",
249
+ },
250
+ hashText: {
251
+ color: colors.accent,
252
+ fontSize: 12,
253
+ fontFamily: "monospace",
254
+ marginBottom: 4,
255
+ },
256
+ copyHint: {
257
+ color: colors.accent,
258
+ fontSize: 10,
259
+ fontStyle: "italic",
260
+ textAlign: "center",
261
+ },
262
+ faucetLink: {
263
+ color: colors.accent,
264
+ textDecorationLine: "underline",
265
+ },
266
+ });
267
+
268
+ const styles = createStyles();
269
+
270
+ return (
271
+ <View style={styles.container}>
272
+ {/* Balance Section */}
273
+ <View style={styles.balanceSection}>
274
+ <Text style={styles.balanceTitle}>Current Balance</Text>
275
+ <Text style={styles.balanceAmount}>
276
+ {formattedBalance === undefined ? "Loading..." : `${formattedBalance} SOL`}
277
+ </Text>
278
+ {!hasBalance && (
279
+ <TouchableOpacity style={styles.faucetButton} onPress={openFaucet}>
280
+ <Text style={styles.faucetButtonText}>Get funds from faucet</Text>
281
+ </TouchableOpacity>
282
+ )}
283
+ </View>
284
+
285
+ {/* Transaction Section */}
286
+ <View style={styles.transactionSection}>
287
+ <Text style={styles.sectionTitle}>Transfer SOL</Text>
288
+ <Text style={styles.sectionDescription}>
289
+ This example transaction sends 1 Lamport from your wallet to yourself on{" "}
290
+ <Text style={styles.faucetLink} onPress={openFaucet}>
291
+ CDP Faucet
292
+ </Text>
293
+ .
294
+ </Text>
295
+
296
+ <TouchableOpacity
297
+ style={[styles.sendButton, (!solanaAddress || isLoading) && styles.sendButtonDisabled]}
298
+ onPress={handleSendTransaction}
299
+ disabled={!solanaAddress || isLoading}
300
+ >
301
+ {isLoading ? (
302
+ <ActivityIndicator color="#fff" />
303
+ ) : (
304
+ <Text style={styles.sendButtonText}>Transfer</Text>
305
+ )}
306
+ </TouchableOpacity>
307
+
308
+ {error ? (
309
+ <View style={styles.errorContainer}>
310
+ <Text style={styles.errorText}>{error}</Text>
311
+ </View>
312
+ ) : null}
313
+
314
+ {transactionSignature ? (
315
+ <View style={styles.successContainer}>
316
+ <Text style={styles.successTitle}>Transfer Complete</Text>
317
+ <View style={styles.hashContainer}>
318
+ <Text style={styles.hashLabel}>Transaction Signature:</Text>
319
+ <TouchableOpacity
320
+ style={styles.hashButton}
321
+ onPress={() =>
322
+ copyToClipboard(
323
+ `https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`,
324
+ "Block Explorer Link",
325
+ )
326
+ }
327
+ >
328
+ <Text style={styles.hashText}>{transactionSignature}</Text>
329
+ <Text style={styles.copyHint}>Tap to copy block explorer link</Text>
330
+ </TouchableOpacity>
331
+ </View>
332
+ </View>
333
+ ) : null}
334
+ </View>
335
+ </View>
336
+ );
337
+ }
338
+
339
+ /**
340
+ * Creates and encodes a Solana transaction.
341
+ *
342
+ * @param address - The address of the sender.
343
+ * @returns The base64 encoded transaction.
344
+ */
345
+ function createAndEncodeTransaction(address: string) {
346
+ const recipientAddress = new PublicKey(SOLANA_FAUCET_ADDRESS);
347
+ const fromPubkey = new PublicKey(address);
348
+ const transferAmount = 1; // 1 Lamport
349
+
350
+ const transaction = new Transaction().add(
351
+ SystemProgram.transfer({
352
+ fromPubkey,
353
+ toPubkey: recipientAddress,
354
+ lamports: transferAmount,
355
+ }),
356
+ );
357
+
358
+ transaction.recentBlockhash = SYSVAR_RECENT_BLOCKHASHES_PUBKEY.toBase58();
359
+ transaction.feePayer = fromPubkey;
360
+
361
+ const serializedTransaction = transaction.serialize({
362
+ requireAllSignatures: false,
363
+ });
364
+
365
+ return Buffer.from(serializedTransaction).toString("base64");
366
+ }
367
+
368
+ export default SolanaTransaction;