@coinbase/create-cdp-app 0.0.35 → 0.0.37
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.
- package/README.md +22 -9
- package/dist/index.js +168 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/template-nextjs/README.md +3 -2
- package/template-nextjs/env.example +4 -2
- package/template-nextjs/package.json +3 -1
- package/template-nextjs/public/sol.svg +13 -0
- package/template-nextjs/src/app/globals.css +6 -0
- package/template-nextjs/src/components/Header.tsx +2 -1
- package/template-nextjs/src/components/Providers.tsx +32 -12
- package/template-nextjs/src/components/SignedInScreen.tsx +63 -15
- package/template-nextjs/src/components/SignedInScreenWithOnramp.tsx +22 -3
- package/template-nextjs/src/components/SolanaTransaction.tsx +157 -0
- package/template-nextjs/src/components/UserBalance.tsx +5 -3
- package/template-react/README.md +2 -1
- package/template-react/env.example +4 -2
- package/template-react/package.json +3 -1
- package/template-react/public/sol.svg +13 -0
- package/template-react/src/Header.tsx +21 -9
- package/template-react/src/SignedInScreen.tsx +66 -13
- package/template-react/src/SolanaTransaction.tsx +158 -0
- package/template-react/src/UserBalance.tsx +29 -10
- package/template-react/src/config.ts +31 -11
- package/template-react/src/index.css +6 -0
- package/template-react/src/main.tsx +2 -2
- package/template-react-native/App.tsx +83 -63
- package/template-react-native/EOATransaction.tsx +35 -22
- package/template-react-native/SmartAccountTransaction.tsx +9 -9
- package/template-react-native/components/SignInForm.tsx +433 -0
- package/template-react-native/env.example +1 -1
- package/template-react-native/types.ts +0 -22
- 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>
|
package/template-react/README.md
CHANGED
|
@@ -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
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# CDP Configuration
|
|
2
2
|
VITE_CDP_PROJECT_ID=example-id
|
|
3
3
|
|
|
4
|
-
# Account type: "
|
|
5
|
-
|
|
4
|
+
# Ethereum Account type: "eoa" for regular accounts, "smart" for Smart Accounts (gasless transactions and improved UX)
|
|
5
|
+
VITE_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE=eoa
|
|
6
|
+
# Whether to create a Solana Account
|
|
7
|
+
VITE_CDP_CREATE_SOLANA_ACCOUNT=false
|
|
6
8
|
|
|
@@ -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 (!
|
|
29
|
+
if (!address) return;
|
|
16
30
|
try {
|
|
17
|
-
await navigator.clipboard.writeText(
|
|
31
|
+
await navigator.clipboard.writeText(address);
|
|
18
32
|
setIsCopied(true);
|
|
19
33
|
} catch (error) {
|
|
20
34
|
console.error(error);
|
|
@@ -29,7 +43,7 @@ function Header() {
|
|
|
29
43
|
return () => clearTimeout(timeout);
|
|
30
44
|
}, [isCopied]);
|
|
31
45
|
|
|
32
|
-
const isSmartAccountsEnabled = import.meta.env.
|
|
46
|
+
const isSmartAccountsEnabled = import.meta.env.VITE_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE === "smart";
|
|
33
47
|
|
|
34
48
|
return (
|
|
35
49
|
<header>
|
|
@@ -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
|
-
{
|
|
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 {
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
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 (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 &&
|
|
55
|
-
<
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
);
|