@coinbase/create-cdp-app 0.0.36 → 0.0.38

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 (31) hide show
  1. package/README.md +22 -9
  2. package/dist/index.js +168 -46
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/template-nextjs/README.md +3 -2
  6. package/template-nextjs/env.example +2 -0
  7. package/template-nextjs/package.json +3 -1
  8. package/template-nextjs/public/sol.svg +13 -0
  9. package/template-nextjs/src/app/globals.css +6 -0
  10. package/template-nextjs/src/components/Providers.tsx +32 -14
  11. package/template-nextjs/src/components/SignedInScreen.tsx +63 -15
  12. package/template-nextjs/src/components/SignedInScreenWithOnramp.tsx +22 -3
  13. package/template-nextjs/src/components/SolanaTransaction.tsx +157 -0
  14. package/template-nextjs/src/components/UserBalance.tsx +5 -3
  15. package/template-react/README.md +2 -1
  16. package/template-react/env.example +3 -0
  17. package/template-react/package.json +3 -1
  18. package/template-react/public/sol.svg +13 -0
  19. package/template-react/src/Header.tsx +20 -8
  20. package/template-react/src/SignedInScreen.tsx +66 -13
  21. package/template-react/src/SolanaTransaction.tsx +158 -0
  22. package/template-react/src/UserBalance.tsx +29 -10
  23. package/template-react/src/config.ts +25 -11
  24. package/template-react/src/index.css +6 -0
  25. package/template-react/src/main.tsx +2 -2
  26. package/template-react-native/App.tsx +79 -62
  27. package/template-react-native/EOATransaction.tsx +35 -22
  28. package/template-react-native/SmartAccountTransaction.tsx +9 -9
  29. package/template-react-native/components/SignInForm.tsx +433 -0
  30. package/template-react-native/types.ts +0 -22
  31. package/template-react-native/components/SignInModal.tsx +0 -342
@@ -0,0 +1,157 @@
1
+ "use client";
2
+
3
+ import { Buffer } from "buffer";
4
+
5
+ import { useSolanaAddress, useSignSolanaTransaction } from "@coinbase/cdp-hooks";
6
+ import { Button } from "@coinbase/cdp-react/components/ui/Button";
7
+ import {
8
+ PublicKey,
9
+ Transaction,
10
+ SystemProgram,
11
+ SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
12
+ } from "@solana/web3.js";
13
+ import { useEffect, useState } from "react";
14
+
15
+ import { IconCheck, IconCopy } from "./Icons";
16
+
17
+ interface Props {
18
+ onSuccess?: () => void;
19
+ }
20
+
21
+ /**
22
+ * Solana transaction component that demonstrates signing transactions.
23
+ *
24
+ * @param props - The component props
25
+ */
26
+ export default function SolanaTransaction(props: Props) {
27
+ const { onSuccess } = props;
28
+ const { solanaAddress } = useSolanaAddress();
29
+ 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
+ }
60
+ };
61
+
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
+ }
70
+ };
71
+
72
+ useEffect(() => {
73
+ if (!isCopied) return;
74
+ const timeout = setTimeout(() => {
75
+ setIsCopied(false);
76
+ }, 2000);
77
+ return () => clearTimeout(timeout);
78
+ }, [isCopied]);
79
+
80
+ 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>
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Creates and encodes a Solana transaction.
130
+ *
131
+ * @param address - The address of the sender.
132
+ * @returns The base64 encoded transaction.
133
+ */
134
+ function createAndEncodeTransaction(address: string) {
135
+ const recipientAddress = new PublicKey(address);
136
+
137
+ const fromPubkey = new PublicKey(address);
138
+
139
+ const transferAmount = 1; // 1 Lamport
140
+
141
+ const transaction = new Transaction().add(
142
+ SystemProgram.transfer({
143
+ fromPubkey,
144
+ toPubkey: recipientAddress,
145
+ lamports: transferAmount,
146
+ }),
147
+ );
148
+
149
+ transaction.recentBlockhash = SYSVAR_RECENT_BLOCKHASHES_PUBKEY.toBase58();
150
+ transaction.feePayer = fromPubkey;
151
+
152
+ const serializedTransaction = transaction.serialize({
153
+ requireAllSignatures: false,
154
+ });
155
+
156
+ return Buffer.from(serializedTransaction).toString("base64");
157
+ }
@@ -16,6 +16,8 @@ interface Props {
16
16
  */
17
17
  export default function UserBalance(props: Props) {
18
18
  const { balance, faucetUrl, faucetName } = props;
19
+ const isSolana = !!process.env.NEXT_PUBLIC_CDP_CREATE_SOLANA_ACCOUNT;
20
+
19
21
  return (
20
22
  <>
21
23
  <h2 className="card-title">Available balance</h2>
@@ -23,15 +25,15 @@ export default function UserBalance(props: Props) {
23
25
  {balance === undefined && <LoadingSkeleton as="span" className="loading--balance" />}
24
26
  {balance !== undefined && (
25
27
  <span className="flex-row-container">
26
- <img src="/eth.svg" alt="" className="balance-icon" />
28
+ <img src={isSolana ? "/sol.svg" : "/eth.svg"} alt="" className="balance-icon" />
27
29
  <span>{balance}</span>
28
- <span className="sr-only">Ethereum</span>
30
+ <span className="sr-only">{isSolana ? "Solana" : "Ethereum"}</span>
29
31
  </span>
30
32
  )}
31
33
  </p>
32
34
  {faucetUrl && faucetName && (
33
35
  <p>
34
- Get testnet ETH from{" "}
36
+ Get testnet {isSolana ? "SOL" : "ETH"} from{" "}
35
37
  <a href={faucetUrl} target="_blank" rel="noopener noreferrer">
36
38
  {faucetName}
37
39
  </a>
@@ -66,7 +66,8 @@ Visit [http://localhost:3000](http://localhost:3000) to see your app.
66
66
 
67
67
  This template comes with:
68
68
  - CDP React components for authentication
69
- - Example transaction components for Base Sepolia
69
+ - Example transaction components for Base Sepolia (EVM) and Solana Devnet
70
+ - Support for EVM EOA, EVM Smart Accounts, and Solana account types
70
71
  - Vite for fast development and building
71
72
  - TypeScript for type safety
72
73
  - ESLint for code quality
@@ -3,3 +3,6 @@ VITE_CDP_PROJECT_ID=example-id
3
3
 
4
4
  # Ethereum Account type: "eoa" for regular accounts, "smart" for Smart Accounts (gasless transactions and improved UX)
5
5
  VITE_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE=eoa
6
+ # Whether to create a Solana Account
7
+ VITE_CDP_CREATE_SOLANA_ACCOUNT=false
8
+
@@ -18,7 +18,9 @@
18
18
  "@coinbase/cdp-react": "latest",
19
19
  "react": "^19.1.0",
20
20
  "react-dom": "^19.1.0",
21
- "viem": "^2.33.0"
21
+ "viem": "^2.33.0",
22
+ "@solana/web3.js": "^1.98.4",
23
+ "buffer": "^6.0.3"
22
24
  },
23
25
  "devDependencies": {
24
26
  "@eslint/js": "^9.30.1",
@@ -0,0 +1,13 @@
1
+ <svg width="101" height="88" viewBox="0 0 101 88" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M100.48 69.3817L83.8068 86.8015C83.4444 87.1799 83.0058 87.4816 82.5185 87.6878C82.0312 87.894 81.5055 88.0003 80.9743 88H1.93563C1.55849 88 1.18957 87.8926 0.874202 87.6912C0.558829 87.4897 0.31074 87.2029 0.160416 86.8659C0.0100923 86.529 -0.0359181 86.1566 0.0280382 85.7945C0.0919944 85.4324 0.263131 85.0964 0.520422 84.8278L17.2061 67.408C17.5676 67.0306 18.0047 66.7295 18.4904 66.5234C18.9762 66.3172 19.5002 66.2104 20.0301 66.2095H99.0644C99.4415 66.2095 99.8104 66.3169 100.126 66.5183C100.441 66.7198 100.689 67.0067 100.84 67.3436C100.99 67.6806 101.036 68.0529 100.972 68.415C100.908 68.7771 100.737 69.1131 100.48 69.3817ZM83.8068 34.3032C83.4444 33.9248 83.0058 33.6231 82.5185 33.4169C82.0312 33.2108 81.5055 33.1045 80.9743 33.1048H1.93563C1.55849 33.1048 1.18957 33.2121 0.874202 33.4136C0.558829 33.6151 0.31074 33.9019 0.160416 34.2388C0.0100923 34.5758 -0.0359181 34.9482 0.0280382 35.3103C0.0919944 35.6723 0.263131 36.0083 0.520422 36.277L17.2061 53.6968C17.5676 54.0742 18.0047 54.3752 18.4904 54.5814C18.9762 54.7875 19.5002 54.8944 20.0301 54.8952H99.0644C99.4415 54.8952 99.8104 54.7879 100.126 54.5864C100.441 54.3849 100.689 54.0981 100.84 53.7612C100.99 53.4242 101.036 53.0518 100.972 52.6897C100.908 52.3277 100.737 51.9917 100.48 51.723L83.8068 34.3032ZM1.93563 21.7905H80.9743C81.5055 21.7907 82.0312 21.6845 82.5185 21.4783C83.0058 21.2721 83.4444 20.9704 83.8068 20.592L100.48 3.17219C100.737 2.90357 100.908 2.56758 100.972 2.2055C101.036 1.84342 100.99 1.47103 100.84 1.13408C100.689 0.79713 100.441 0.510296 100.126 0.308823C99.8104 0.107349 99.4415 1.24074e-05 99.0644 0L20.0301 0C19.5002 0.000878397 18.9762 0.107699 18.4904 0.313848C18.0047 0.519998 17.5676 0.821087 17.2061 1.19848L0.524723 18.6183C0.267681 18.8866 0.0966198 19.2223 0.0325185 19.5839C-0.0315829 19.9456 0.0140624 20.3177 0.163856 20.6545C0.31365 20.9913 0.561081 21.2781 0.875804 21.4799C1.19053 21.6817 1.55886 21.7896 1.93563 21.7905Z" fill="url(#paint0_linear_174_4403)"/>
3
+ <defs>
4
+ <linearGradient id="paint0_linear_174_4403" x1="8.52558" y1="90.0973" x2="88.9933" y2="-3.01622" gradientUnits="userSpaceOnUse">
5
+ <stop offset="0.08" stop-color="#9945FF"/>
6
+ <stop offset="0.3" stop-color="#8752F3"/>
7
+ <stop offset="0.5" stop-color="#5497D5"/>
8
+ <stop offset="0.6" stop-color="#43B4CA"/>
9
+ <stop offset="0.72" stop-color="#28E0B9"/>
10
+ <stop offset="0.97" stop-color="#19FB9B"/>
11
+ </linearGradient>
12
+ </defs>
13
+ </svg>
@@ -1,7 +1,8 @@
1
- import { useEvmAddress } from "@coinbase/cdp-hooks";
1
+ import { useEvmAddress, useSolanaAddress } from "@coinbase/cdp-hooks";
2
2
  import { AuthButton } from "@coinbase/cdp-react/components/AuthButton";
3
- import { useEffect, useState } from "react";
3
+ import { useCallback, useEffect, useState } from "react";
4
4
 
5
+ import { CDP_CONFIG } from "./config";
5
6
  import { IconCheck, IconCopy, IconUser } from "./Icons";
6
7
 
7
8
  /**
@@ -9,12 +10,25 @@ import { IconCheck, IconCopy, IconUser } from "./Icons";
9
10
  */
10
11
  function Header() {
11
12
  const { evmAddress } = useEvmAddress();
13
+ const { solanaAddress } = useSolanaAddress();
14
+ const isSolana = !!CDP_CONFIG.solana;
15
+ const address = isSolana ? solanaAddress : evmAddress;
12
16
  const [isCopied, setIsCopied] = useState(false);
13
17
 
18
+ const formatAddress = useCallback(
19
+ (address: string) => {
20
+ if (!address) return "";
21
+ return isSolana
22
+ ? `${address.slice(0, 4)}...${address.slice(-4)}`
23
+ : `${address.slice(0, 6)}...${address.slice(-4)}`;
24
+ },
25
+ [isSolana],
26
+ );
27
+
14
28
  const copyAddress = async () => {
15
- if (!evmAddress) return;
29
+ if (!address) return;
16
30
  try {
17
- await navigator.clipboard.writeText(evmAddress);
31
+ await navigator.clipboard.writeText(address);
18
32
  setIsCopied(true);
19
33
  } catch (error) {
20
34
  console.error(error);
@@ -39,7 +53,7 @@ function Header() {
39
53
  {isSmartAccountsEnabled && <span className="smart-badge">SMART</span>}
40
54
  </div>
41
55
  <div className="user-info flex-row-container">
42
- {evmAddress && (
56
+ {address && (
43
57
  <button
44
58
  aria-label="copy wallet address"
45
59
  className="flex-row-container copy-address-button"
@@ -52,9 +66,7 @@ function Header() {
52
66
  </>
53
67
  )}
54
68
  {isCopied && <IconCheck className="user-icon user-icon--check" />}
55
- <span className="wallet-address">
56
- {evmAddress.slice(0, 6)}...{evmAddress.slice(-4)}
57
- </span>
69
+ <span className="wallet-address">{formatAddress(address)}</span>
58
70
  </button>
59
71
  )}
60
72
  <AuthButton />
@@ -1,12 +1,30 @@
1
- import { useEvmAddress, useIsSignedIn } from "@coinbase/cdp-hooks";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
1
+ import { useEvmAddress, useSolanaAddress, useIsSignedIn } from "@coinbase/cdp-hooks";
2
+ import { Connection, clusterApiUrl, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
3
+ import { useCallback, useEffect, useMemo, useState, lazy, Suspense } from "react";
3
4
  import { createPublicClient, http, formatEther } from "viem";
4
5
  import { baseSepolia } from "viem/chains";
5
6
 
7
+ import { CDP_CONFIG } from "./config";
6
8
  import Header from "./Header";
7
- import Transaction from "./Transaction";
9
+ import Loading from "./Loading";
8
10
  import UserBalance from "./UserBalance";
9
11
 
12
+ const isSolana = !!CDP_CONFIG.solana;
13
+ const isSmartAccount = CDP_CONFIG.ethereum?.createOnLogin === "smart";
14
+
15
+ // Dynamically determine component path to avoid Vite static analysis
16
+ const getComponentPath = () => {
17
+ if (isSolana) {
18
+ return "./SolanaTransaction";
19
+ } else if (isSmartAccount) {
20
+ return "./SmartAccountTransaction";
21
+ } else {
22
+ return "./EOATransaction";
23
+ }
24
+ };
25
+
26
+ const TransactionComponent = lazy(() => import(/* @vite-ignore */ getComponentPath()));
27
+
10
28
  /**
11
29
  * Create a viem client to access user's balance on the Base Sepolia network
12
30
  */
@@ -15,26 +33,47 @@ const client = createPublicClient({
15
33
  transport: http(),
16
34
  });
17
35
 
36
+ /**
37
+ * Create a Solana connection to access user's balance on Solana Devnet
38
+ */
39
+ const solanaConnection = new Connection(clusterApiUrl("devnet"));
40
+
18
41
  /**
19
42
  * The Signed In screen
20
43
  */
21
44
  function SignedInScreen() {
22
45
  const { isSignedIn } = useIsSignedIn();
23
46
  const { evmAddress } = useEvmAddress();
47
+ const { solanaAddress } = useSolanaAddress();
24
48
  const [balance, setBalance] = useState<bigint | undefined>(undefined);
25
49
 
50
+ const isSolana = !!CDP_CONFIG.solana;
51
+ const address = isSolana ? solanaAddress : evmAddress;
52
+
26
53
  const formattedBalance = useMemo(() => {
27
54
  if (balance === undefined) return undefined;
28
- return formatEther(balance);
29
- }, [balance]);
55
+ if (isSolana) {
56
+ // Convert lamports to SOL
57
+ return formatSol(Number(balance));
58
+ } else {
59
+ // Convert wei to ETH
60
+ return formatEther(balance);
61
+ }
62
+ }, [balance, isSolana]);
30
63
 
31
64
  const getBalance = useCallback(async () => {
32
- if (!evmAddress) return;
33
- const balance = await client.getBalance({
34
- address: evmAddress,
35
- });
36
- setBalance(balance);
37
- }, [evmAddress]);
65
+ if (isSolana && solanaAddress) {
66
+ // Get Solana balance in lamports
67
+ const lamports = await solanaConnection.getBalance(new PublicKey(solanaAddress));
68
+ setBalance(BigInt(lamports));
69
+ } else if (!isSolana && evmAddress) {
70
+ // Get EVM balance in wei
71
+ const weiBalance = await client.getBalance({
72
+ address: evmAddress,
73
+ });
74
+ setBalance(weiBalance);
75
+ }
76
+ }, [evmAddress, solanaAddress, isSolana]);
38
77
 
39
78
  useEffect(() => {
40
79
  getBalance();
@@ -51,8 +90,10 @@ function SignedInScreen() {
51
90
  <UserBalance balance={formattedBalance} />
52
91
  </div>
53
92
  <div className="card card--transaction">
54
- {isSignedIn && evmAddress && (
55
- <Transaction balance={formattedBalance} onSuccess={getBalance} />
93
+ {isSignedIn && address && (
94
+ <Suspense fallback={<Loading />}>
95
+ <TransactionComponent balance={formattedBalance} onSuccess={getBalance} />
96
+ </Suspense>
56
97
  )}
57
98
  </div>
58
99
  </div>
@@ -61,4 +102,16 @@ function SignedInScreen() {
61
102
  );
62
103
  }
63
104
 
105
+ /**
106
+ * Format a Solana balance.
107
+ *
108
+ * @param lamports - The balance in lamports.
109
+ * @returns The formatted balance.
110
+ */
111
+ function formatSol(lamports: number) {
112
+ const maxDecimalPlaces = 9;
113
+ const roundedStr = (lamports / LAMPORTS_PER_SOL).toFixed(maxDecimalPlaces);
114
+ return roundedStr.replace(/0+$/, "").replace(/\.$/, "");
115
+ }
116
+
64
117
  export default SignedInScreen;
@@ -0,0 +1,158 @@
1
+ import { Buffer } from "buffer";
2
+
3
+ import { useSolanaAddress, useSignSolanaTransaction } from "@coinbase/cdp-hooks";
4
+ import { Button } from "@coinbase/cdp-react/components/ui/Button";
5
+ import {
6
+ PublicKey,
7
+ Transaction,
8
+ SystemProgram,
9
+ SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
10
+ } from "@solana/web3.js";
11
+ import { useEffect, useState } from "react";
12
+
13
+ import { IconCheck, IconCopy } from "./Icons";
14
+
15
+ interface Props {
16
+ onSuccess?: () => void;
17
+ }
18
+
19
+ /**
20
+ * Solana transaction component that demonstrates signing transactions.
21
+ *
22
+ * @param props - The component props
23
+ */
24
+ function SolanaTransaction(props: Props) {
25
+ const { onSuccess } = props;
26
+ const { solanaAddress } = useSolanaAddress();
27
+ 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
+ }
59
+ };
60
+
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
+ }
69
+ };
70
+
71
+ useEffect(() => {
72
+ if (!isCopied) return;
73
+ const timeout = setTimeout(() => {
74
+ setIsCopied(false);
75
+ }, 2000);
76
+ return () => clearTimeout(timeout);
77
+ }, [isCopied]);
78
+
79
+ 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>
117
+ </Button>
118
+ <small>Click to copy signed transaction</small>
119
+ </div>
120
+ </div>
121
+ )}
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Creates and encodes a Solana transaction.
129
+ *
130
+ * @param address - The address of the sender.
131
+ * @returns The base64 encoded transaction.
132
+ */
133
+ function createAndEncodeTransaction(address: string) {
134
+ const recipientAddress = new PublicKey(address);
135
+
136
+ const fromPubkey = new PublicKey(address);
137
+
138
+ const transferAmount = 1; // 1 Lamport
139
+
140
+ const transaction = new Transaction().add(
141
+ SystemProgram.transfer({
142
+ fromPubkey,
143
+ toPubkey: recipientAddress,
144
+ lamports: transferAmount,
145
+ }),
146
+ );
147
+
148
+ transaction.recentBlockhash = SYSVAR_RECENT_BLOCKHASHES_PUBKEY.toBase58();
149
+ transaction.feePayer = fromPubkey;
150
+
151
+ const serializedTransaction = transaction.serialize({
152
+ requireAllSignatures: false,
153
+ });
154
+
155
+ return Buffer.from(serializedTransaction).toString("base64");
156
+ }
157
+
158
+ export default SolanaTransaction;
@@ -1,5 +1,7 @@
1
1
  import { LoadingSkeleton } from "@coinbase/cdp-react/components/ui/LoadingSkeleton";
2
2
 
3
+ import { CDP_CONFIG } from "./config";
4
+
3
5
  interface Props {
4
6
  balance?: string;
5
7
  }
@@ -13,6 +15,8 @@ interface Props {
13
15
  */
14
16
  function UserBalance(props: Props) {
15
17
  const { balance } = props;
18
+ const isSolana = !!CDP_CONFIG.solana;
19
+
16
20
  return (
17
21
  <>
18
22
  <h2 className="card-title">Available balance</h2>
@@ -20,21 +24,36 @@ function UserBalance(props: Props) {
20
24
  {balance === undefined && <LoadingSkeleton as="span" className="loading--balance" />}
21
25
  {balance !== undefined && (
22
26
  <span className="flex-row-container">
23
- <img src="/eth.svg" alt="" className="balance-icon" />
27
+ <img src={isSolana ? "/sol.svg" : "/eth.svg"} alt="" className="balance-icon" />
24
28
  <span>{balance}</span>
25
- <span className="sr-only">Ethereum</span>
29
+ <span className="sr-only">{isSolana ? "Solana" : "Ethereum"}</span>
26
30
  </span>
27
31
  )}
28
32
  </p>
29
33
  <p>
30
- Get testnet ETH from{" "}
31
- <a
32
- href="https://portal.cdp.coinbase.com/products/faucet"
33
- target="_blank"
34
- rel="noopener noreferrer"
35
- >
36
- Base Sepolia Faucet
37
- </a>
34
+ {isSolana ? (
35
+ <>
36
+ Get testnet SOL from{" "}
37
+ <a
38
+ href="https://portal.cdp.coinbase.com/products/faucet?network=solana-devnet"
39
+ target="_blank"
40
+ rel="noopener noreferrer"
41
+ >
42
+ Solana Devnet Faucet
43
+ </a>
44
+ </>
45
+ ) : (
46
+ <>
47
+ Get testnet ETH from{" "}
48
+ <a
49
+ href="https://portal.cdp.coinbase.com/products/faucet"
50
+ target="_blank"
51
+ rel="noopener noreferrer"
52
+ >
53
+ Base Sepolia Faucet
54
+ </a>
55
+ </>
56
+ )}
38
57
  </p>
39
58
  </>
40
59
  );
@@ -1,20 +1,34 @@
1
- import { type Config } from "@coinbase/cdp-hooks";
2
- import { type AppConfig } from "@coinbase/cdp-react";
1
+ import { type Config } from "@coinbase/cdp-react";
3
2
 
4
- const ethereumAccountType =
5
- import.meta.env.VITE_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE === "smart" ? "smart" : "eoa";
3
+ const ethereumAccountType = import.meta.env.VITE_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE
4
+ ? import.meta.env.VITE_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE === "smart"
5
+ ? "smart"
6
+ : "eoa"
7
+ : undefined;
6
8
 
7
- export const CDP_CONFIG: Config = {
9
+ const solanaAccountType = import.meta.env.VITE_CDP_CREATE_SOLANA_ACCOUNT
10
+ ? import.meta.env.VITE_CDP_CREATE_SOLANA_ACCOUNT === "true"
11
+ : undefined;
12
+
13
+ if (!ethereumAccountType && !solanaAccountType) {
14
+ throw new Error(
15
+ "Either VITE_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE or VITE_CDP_CREATE_SOLANA_ACCOUNT must be defined",
16
+ );
17
+ }
18
+
19
+ export const CDP_CONFIG = {
8
20
  projectId: import.meta.env.VITE_CDP_PROJECT_ID,
9
21
  ...(ethereumAccountType && {
10
22
  ethereum: {
11
23
  createOnLogin: ethereumAccountType,
12
24
  },
13
25
  }),
14
- };
15
-
16
- export const APP_CONFIG: AppConfig = {
17
- name: "CDP React StarterKit",
18
- logoUrl: "http://localhost:3000/logo.svg",
26
+ ...(solanaAccountType && {
27
+ solana: {
28
+ createOnLogin: solanaAccountType,
29
+ },
30
+ }),
31
+ appName: "CDP React StarterKit",
32
+ appLogoUrl: "http://localhost:3000/logo.svg",
19
33
  authMethods: ["email", "sms"],
20
- };
34
+ } as Config;