@coinbase/create-cdp-app 0.0.43 → 0.0.44
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/dist/index.js +119 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/template-nextjs/src/components/{Header.tsx → evm-eoa/Header.tsx} +0 -4
- package/template-nextjs/src/components/evm-eoa/SignedInScreen.tsx +68 -0
- package/template-nextjs/src/components/evm-eoa/SignedInScreenWithOnramp.tsx +117 -0
- package/template-nextjs/src/components/{UserBalance.tsx → evm-eoa/UserBalance.tsx} +3 -4
- package/template-nextjs/src/components/evm-smart/Header.tsx +64 -0
- package/template-nextjs/src/components/evm-smart/SignedInScreen.tsx +68 -0
- package/template-nextjs/src/components/evm-smart/SignedInScreenWithOnramp.tsx +120 -0
- package/template-nextjs/src/components/evm-smart/UserBalance.tsx +43 -0
- package/template-nextjs/src/components/solana/Header.tsx +63 -0
- package/template-nextjs/src/components/solana/SignedInScreen.tsx +74 -0
- package/template-nextjs/src/components/solana/SignedInScreenWithOnramp.tsx +121 -0
- package/template-nextjs/src/components/solana/UserBalance.tsx +43 -0
- package/template-react/src/evm-eoa/Header.tsx +67 -0
- package/template-react/src/evm-eoa/SignedInScreen.tsx +64 -0
- package/template-react/src/{UserBalance.tsx → evm-eoa/UserBalance.tsx} +10 -28
- package/template-react/src/evm-smart/Header.tsx +68 -0
- package/template-react/src/evm-smart/SignedInScreen.tsx +64 -0
- package/template-react/src/evm-smart/UserBalance.tsx +44 -0
- package/template-react/src/{Header.tsx → solana/Header.tsx} +9 -21
- package/template-react/src/solana/SignedInScreen.tsx +70 -0
- package/template-react/src/solana/UserBalance.tsx +44 -0
- package/template-react-native/{components → evm-eoa}/WalletHeader.tsx +10 -21
- package/template-react-native/evm-smart/WalletHeader.tsx +115 -0
- package/template-react-native/solana/WalletHeader.tsx +111 -0
- package/template-nextjs/src/components/SignedInScreen.tsx +0 -116
- package/template-nextjs/src/components/SignedInScreenWithOnramp.tsx +0 -239
- package/template-react/src/SignedInScreen.tsx +0 -116
- package/template-react-native/Transaction.tsx +0 -101
- /package/template-nextjs/src/components/{EOATransaction.tsx → evm-eoa/EOATransaction.tsx} +0 -0
- /package/template-nextjs/src/components/{SmartAccountTransaction.tsx → evm-smart/SmartAccountTransaction.tsx} +0 -0
- /package/template-nextjs/src/components/{SolanaTransaction.tsx → solana/SolanaTransaction.tsx} +0 -0
- /package/template-react/src/{EOATransaction.tsx → evm-eoa/EOATransaction.tsx} +0 -0
- /package/template-react/src/{SmartAccountTransaction.tsx → evm-smart/SmartAccountTransaction.tsx} +0 -0
- /package/template-react/src/{SolanaTransaction.tsx → solana/SolanaTransaction.tsx} +0 -0
- /package/template-react-native/{EOATransaction.tsx → evm-eoa/EOATransaction.tsx} +0 -0
- /package/template-react-native/{SmartAccountTransaction.tsx → evm-smart/SmartAccountTransaction.tsx} +0 -0
- /package/template-react-native/{SolanaTransaction.tsx → solana/SolanaTransaction.tsx} +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useIsSignedIn, useSolanaAddress } from "@coinbase/cdp-hooks";
|
|
4
|
+
import { Connection, clusterApiUrl, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
6
|
+
|
|
7
|
+
import Header from "./Header";
|
|
8
|
+
import SolanaTransaction from "./SolanaTransaction";
|
|
9
|
+
import UserBalance from "./UserBalance";
|
|
10
|
+
|
|
11
|
+
import FundWallet from "@/components/FundWallet";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a Solana connection to access user's balance on Solana Mainnet
|
|
15
|
+
*/
|
|
16
|
+
const solanaMainnetConnection = new Connection(clusterApiUrl("mainnet-beta"));
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a Solana connection to access user's balance on Solana Devnet
|
|
20
|
+
*/
|
|
21
|
+
const solanaDevnetConnection = new Connection(clusterApiUrl("devnet"));
|
|
22
|
+
|
|
23
|
+
const useSolanaBalance = (address: string | null, connection: Connection, poll = false) => {
|
|
24
|
+
const [balance, setBalance] = useState<bigint | undefined>(undefined);
|
|
25
|
+
|
|
26
|
+
const formattedBalance = useMemo(() => {
|
|
27
|
+
if (balance === undefined) return undefined;
|
|
28
|
+
// Convert lamports to SOL
|
|
29
|
+
return formatSol(Number(balance));
|
|
30
|
+
}, [balance]);
|
|
31
|
+
|
|
32
|
+
const getBalance = useCallback(async () => {
|
|
33
|
+
if (!address) return;
|
|
34
|
+
try {
|
|
35
|
+
const lamports = await connection.getBalance(new PublicKey(address));
|
|
36
|
+
setBalance(BigInt(lamports));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Error fetching Solana balance:", error);
|
|
39
|
+
setBalance(BigInt(0));
|
|
40
|
+
}
|
|
41
|
+
}, [address, connection]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!poll) {
|
|
45
|
+
getBalance();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const interval = setInterval(getBalance, 500);
|
|
49
|
+
return () => clearInterval(interval);
|
|
50
|
+
}, [getBalance, poll]);
|
|
51
|
+
|
|
52
|
+
return { balance, formattedBalance, getBalance };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format a Solana balance.
|
|
57
|
+
*
|
|
58
|
+
* @param lamports - The balance in lamports.
|
|
59
|
+
* @returns The formatted balance.
|
|
60
|
+
*/
|
|
61
|
+
function formatSol(lamports: number) {
|
|
62
|
+
const maxDecimalPlaces = 9;
|
|
63
|
+
const roundedStr = (lamports / LAMPORTS_PER_SOL).toFixed(maxDecimalPlaces);
|
|
64
|
+
return roundedStr.replace(/0+$/, "").replace(/\.$/, "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The Signed In screen with onramp support
|
|
69
|
+
*/
|
|
70
|
+
export default function SignedInScreen() {
|
|
71
|
+
const { isSignedIn } = useIsSignedIn();
|
|
72
|
+
const { solanaAddress } = useSolanaAddress();
|
|
73
|
+
|
|
74
|
+
const { formattedBalance: formattedBalanceSolana, getBalance: getBalanceSolana } =
|
|
75
|
+
useSolanaBalance(solanaAddress, solanaMainnetConnection);
|
|
76
|
+
const { formattedBalance: formattedBalanceSolanaDevnet, getBalance: getBalanceSolanaDevnet } =
|
|
77
|
+
useSolanaBalance(solanaAddress, solanaDevnetConnection, true);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<Header />
|
|
82
|
+
<main className="main flex-col-container flex-grow">
|
|
83
|
+
<p className="page-heading">Fund your Solana wallet on Mainnet</p>
|
|
84
|
+
<div className="main-inner flex-col-container">
|
|
85
|
+
<div className="card card--user-balance">
|
|
86
|
+
<UserBalance balance={formattedBalanceSolana} />
|
|
87
|
+
</div>
|
|
88
|
+
<div className="card card--transaction">
|
|
89
|
+
{isSignedIn && (
|
|
90
|
+
<FundWallet
|
|
91
|
+
onSuccess={getBalanceSolana}
|
|
92
|
+
network="solana"
|
|
93
|
+
cryptoCurrency="sol"
|
|
94
|
+
destinationAddress={solanaAddress}
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<hr className="page-divider" />
|
|
100
|
+
<p className="page-heading">Send a Solana transaction on Devnet</p>
|
|
101
|
+
<div className="main-inner flex-col-container">
|
|
102
|
+
<div className="card card--user-balance">
|
|
103
|
+
<UserBalance
|
|
104
|
+
balance={formattedBalanceSolanaDevnet}
|
|
105
|
+
faucetName="Solana Devnet Faucet"
|
|
106
|
+
faucetUrl="https://portal.cdp.coinbase.com/products/faucet?network=solana-devnet"
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="card card--transaction">
|
|
110
|
+
{isSignedIn && (
|
|
111
|
+
<SolanaTransaction
|
|
112
|
+
balance={formattedBalanceSolanaDevnet}
|
|
113
|
+
onSuccess={getBalanceSolanaDevnet}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</main>
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { LoadingSkeleton } from "@coinbase/cdp-react/components/ui/LoadingSkeleton";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
balance?: string;
|
|
6
|
+
faucetUrl?: string;
|
|
7
|
+
faucetName?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A component that displays the user's balance.
|
|
12
|
+
*
|
|
13
|
+
* @param {Props} props - The props for the UserBalance component.
|
|
14
|
+
* @param {string} [props.balance] - The user's balance.
|
|
15
|
+
* @returns A component that displays the user's balance.
|
|
16
|
+
*/
|
|
17
|
+
export default function UserBalance(props: Props) {
|
|
18
|
+
const { balance, faucetUrl, faucetName } = props;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<h2 className="card-title">Available balance</h2>
|
|
23
|
+
<p className="user-balance flex-col-container flex-grow">
|
|
24
|
+
{balance === undefined && <LoadingSkeleton as="span" className="loading--balance" />}
|
|
25
|
+
{balance !== undefined && (
|
|
26
|
+
<span className="flex-row-container">
|
|
27
|
+
<img src="/sol.svg" alt="" className="balance-icon" />
|
|
28
|
+
<span>{balance}</span>
|
|
29
|
+
<span className="sr-only">Solana</span>
|
|
30
|
+
</span>
|
|
31
|
+
)}
|
|
32
|
+
</p>
|
|
33
|
+
{faucetUrl && faucetName && (
|
|
34
|
+
<p>
|
|
35
|
+
Get testnet SOL from{" "}
|
|
36
|
+
<a href={faucetUrl} target="_blank" rel="noopener noreferrer">
|
|
37
|
+
{faucetName}
|
|
38
|
+
</a>
|
|
39
|
+
</p>
|
|
40
|
+
)}
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEvmAddress } from "@coinbase/cdp-hooks";
|
|
2
|
+
import { AuthButton } from "@coinbase/cdp-react/components/AuthButton";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { IconCheck, IconCopy, IconUser } from "./Icons";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Header component
|
|
9
|
+
*/
|
|
10
|
+
function Header() {
|
|
11
|
+
const { evmAddress } = useEvmAddress();
|
|
12
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
13
|
+
|
|
14
|
+
const formatAddress = useCallback((address: string) => {
|
|
15
|
+
if (!address) return "";
|
|
16
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
const copyAddress = async () => {
|
|
20
|
+
if (!evmAddress) return;
|
|
21
|
+
try {
|
|
22
|
+
await navigator.clipboard.writeText(evmAddress);
|
|
23
|
+
setIsCopied(true);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!isCopied) return;
|
|
31
|
+
const timeout = setTimeout(() => {
|
|
32
|
+
setIsCopied(false);
|
|
33
|
+
}, 2000);
|
|
34
|
+
return () => clearTimeout(timeout);
|
|
35
|
+
}, [isCopied]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<header>
|
|
39
|
+
<div className="header-inner">
|
|
40
|
+
<div className="title-container">
|
|
41
|
+
<h1 className="site-title">CDP React StarterKit</h1>
|
|
42
|
+
</div>
|
|
43
|
+
<div className="user-info flex-row-container">
|
|
44
|
+
{evmAddress && (
|
|
45
|
+
<button
|
|
46
|
+
aria-label="copy wallet address"
|
|
47
|
+
className="flex-row-container copy-address-button"
|
|
48
|
+
onClick={copyAddress}
|
|
49
|
+
>
|
|
50
|
+
{!isCopied && (
|
|
51
|
+
<>
|
|
52
|
+
<IconUser className="user-icon user-icon--user" />
|
|
53
|
+
<IconCopy className="user-icon user-icon--copy" />
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
{isCopied && <IconCheck className="user-icon user-icon--check" />}
|
|
57
|
+
<span className="wallet-address">{formatAddress(evmAddress)}</span>
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
60
|
+
<AuthButton />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</header>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default Header;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEvmAddress, useIsSignedIn } from "@coinbase/cdp-hooks";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { createPublicClient, http, formatEther } from "viem";
|
|
4
|
+
import { baseSepolia } from "viem/chains";
|
|
5
|
+
|
|
6
|
+
import EOATransaction from "./EOATransaction";
|
|
7
|
+
import Header from "./Header";
|
|
8
|
+
import UserBalance from "./UserBalance";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a viem client to access user's balance on the Base Sepolia network
|
|
12
|
+
*/
|
|
13
|
+
const client = createPublicClient({
|
|
14
|
+
chain: baseSepolia,
|
|
15
|
+
transport: http(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The Signed In screen
|
|
20
|
+
*/
|
|
21
|
+
function SignedInScreen() {
|
|
22
|
+
const { isSignedIn } = useIsSignedIn();
|
|
23
|
+
const { evmAddress } = useEvmAddress();
|
|
24
|
+
const [balance, setBalance] = useState<bigint | undefined>(undefined);
|
|
25
|
+
|
|
26
|
+
const formattedBalance = useMemo(() => {
|
|
27
|
+
if (balance === undefined) return undefined;
|
|
28
|
+
return formatEther(balance);
|
|
29
|
+
}, [balance]);
|
|
30
|
+
|
|
31
|
+
const getBalance = useCallback(async () => {
|
|
32
|
+
if (!evmAddress) return;
|
|
33
|
+
const weiBalance = await client.getBalance({
|
|
34
|
+
address: evmAddress,
|
|
35
|
+
});
|
|
36
|
+
setBalance(weiBalance);
|
|
37
|
+
}, [evmAddress]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
getBalance();
|
|
41
|
+
const interval = setInterval(getBalance, 500);
|
|
42
|
+
return () => clearInterval(interval);
|
|
43
|
+
}, [getBalance]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<Header />
|
|
48
|
+
<main className="main flex-col-container flex-grow">
|
|
49
|
+
<div className="main-inner flex-col-container">
|
|
50
|
+
<div className="card card--user-balance">
|
|
51
|
+
<UserBalance balance={formattedBalance} />
|
|
52
|
+
</div>
|
|
53
|
+
<div className="card card--transaction">
|
|
54
|
+
{isSignedIn && evmAddress && (
|
|
55
|
+
<EOATransaction balance={formattedBalance} onSuccess={getBalance} />
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</main>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default SignedInScreen;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { LoadingSkeleton } from "@coinbase/cdp-react/components/ui/LoadingSkeleton";
|
|
2
2
|
|
|
3
|
-
import { CDP_CONFIG } from "./config";
|
|
4
|
-
|
|
5
3
|
interface Props {
|
|
6
4
|
balance?: string;
|
|
7
5
|
}
|
|
@@ -15,7 +13,6 @@ interface Props {
|
|
|
15
13
|
*/
|
|
16
14
|
function UserBalance(props: Props) {
|
|
17
15
|
const { balance } = props;
|
|
18
|
-
const isSolana = !!CDP_CONFIG.solana;
|
|
19
16
|
|
|
20
17
|
return (
|
|
21
18
|
<>
|
|
@@ -24,36 +21,21 @@ function UserBalance(props: Props) {
|
|
|
24
21
|
{balance === undefined && <LoadingSkeleton as="span" className="loading--balance" />}
|
|
25
22
|
{balance !== undefined && (
|
|
26
23
|
<span className="flex-row-container">
|
|
27
|
-
<img src=
|
|
24
|
+
<img src="/eth.svg" alt="" className="balance-icon" />
|
|
28
25
|
<span>{balance}</span>
|
|
29
|
-
<span className="sr-only">
|
|
26
|
+
<span className="sr-only">Ethereum</span>
|
|
30
27
|
</span>
|
|
31
28
|
)}
|
|
32
29
|
</p>
|
|
33
30
|
<p>
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
)}
|
|
31
|
+
Get testnet ETH from{" "}
|
|
32
|
+
<a
|
|
33
|
+
href="https://portal.cdp.coinbase.com/products/faucet"
|
|
34
|
+
target="_blank"
|
|
35
|
+
rel="noopener noreferrer"
|
|
36
|
+
>
|
|
37
|
+
Base Sepolia Faucet
|
|
38
|
+
</a>
|
|
57
39
|
</p>
|
|
58
40
|
</>
|
|
59
41
|
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useEvmAddress } from "@coinbase/cdp-hooks";
|
|
2
|
+
import { AuthButton } from "@coinbase/cdp-react/components/AuthButton";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { IconCheck, IconCopy, IconUser } from "./Icons";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Header component
|
|
9
|
+
*/
|
|
10
|
+
function Header() {
|
|
11
|
+
const { evmAddress } = useEvmAddress();
|
|
12
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
13
|
+
|
|
14
|
+
const formatAddress = useCallback((address: string) => {
|
|
15
|
+
if (!address) return "";
|
|
16
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
const copyAddress = async () => {
|
|
20
|
+
if (!evmAddress) return;
|
|
21
|
+
try {
|
|
22
|
+
await navigator.clipboard.writeText(evmAddress);
|
|
23
|
+
setIsCopied(true);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!isCopied) return;
|
|
31
|
+
const timeout = setTimeout(() => {
|
|
32
|
+
setIsCopied(false);
|
|
33
|
+
}, 2000);
|
|
34
|
+
return () => clearTimeout(timeout);
|
|
35
|
+
}, [isCopied]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<header>
|
|
39
|
+
<div className="header-inner">
|
|
40
|
+
<div className="title-container">
|
|
41
|
+
<h1 className="site-title">CDP React StarterKit</h1>
|
|
42
|
+
<span className="smart-badge">SMART</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="user-info flex-row-container">
|
|
45
|
+
{evmAddress && (
|
|
46
|
+
<button
|
|
47
|
+
aria-label="copy wallet address"
|
|
48
|
+
className="flex-row-container copy-address-button"
|
|
49
|
+
onClick={copyAddress}
|
|
50
|
+
>
|
|
51
|
+
{!isCopied && (
|
|
52
|
+
<>
|
|
53
|
+
<IconUser className="user-icon user-icon--user" />
|
|
54
|
+
<IconCopy className="user-icon user-icon--copy" />
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
{isCopied && <IconCheck className="user-icon user-icon--check" />}
|
|
58
|
+
<span className="wallet-address">{formatAddress(evmAddress)}</span>
|
|
59
|
+
</button>
|
|
60
|
+
)}
|
|
61
|
+
<AuthButton />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</header>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default Header;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEvmAddress, useIsSignedIn } from "@coinbase/cdp-hooks";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { createPublicClient, http, formatEther } from "viem";
|
|
4
|
+
import { baseSepolia } from "viem/chains";
|
|
5
|
+
|
|
6
|
+
import Header from "./Header";
|
|
7
|
+
import SmartAccountTransaction from "./SmartAccountTransaction";
|
|
8
|
+
import UserBalance from "./UserBalance";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a viem client to access user's balance on the Base Sepolia network
|
|
12
|
+
*/
|
|
13
|
+
const client = createPublicClient({
|
|
14
|
+
chain: baseSepolia,
|
|
15
|
+
transport: http(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The Signed In screen
|
|
20
|
+
*/
|
|
21
|
+
function SignedInScreen() {
|
|
22
|
+
const { isSignedIn } = useIsSignedIn();
|
|
23
|
+
const { evmAddress } = useEvmAddress();
|
|
24
|
+
const [balance, setBalance] = useState<bigint | undefined>(undefined);
|
|
25
|
+
|
|
26
|
+
const formattedBalance = useMemo(() => {
|
|
27
|
+
if (balance === undefined) return undefined;
|
|
28
|
+
return formatEther(balance);
|
|
29
|
+
}, [balance]);
|
|
30
|
+
|
|
31
|
+
const getBalance = useCallback(async () => {
|
|
32
|
+
if (!evmAddress) return;
|
|
33
|
+
const weiBalance = await client.getBalance({
|
|
34
|
+
address: evmAddress,
|
|
35
|
+
});
|
|
36
|
+
setBalance(weiBalance);
|
|
37
|
+
}, [evmAddress]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
getBalance();
|
|
41
|
+
const interval = setInterval(getBalance, 500);
|
|
42
|
+
return () => clearInterval(interval);
|
|
43
|
+
}, [getBalance]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<Header />
|
|
48
|
+
<main className="main flex-col-container flex-grow">
|
|
49
|
+
<div className="main-inner flex-col-container">
|
|
50
|
+
<div className="card card--user-balance">
|
|
51
|
+
<UserBalance balance={formattedBalance} />
|
|
52
|
+
</div>
|
|
53
|
+
<div className="card card--transaction">
|
|
54
|
+
{isSignedIn && evmAddress && (
|
|
55
|
+
<SmartAccountTransaction balance={formattedBalance} onSuccess={getBalance} />
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</main>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default SignedInScreen;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LoadingSkeleton } from "@coinbase/cdp-react/components/ui/LoadingSkeleton";
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
balance?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A component that displays the user's balance.
|
|
9
|
+
*
|
|
10
|
+
* @param {Props} props - The props for the UserBalance component.
|
|
11
|
+
* @param {string} [props.balance] - The user's balance.
|
|
12
|
+
* @returns A component that displays the user's balance.
|
|
13
|
+
*/
|
|
14
|
+
function UserBalance(props: Props) {
|
|
15
|
+
const { balance } = props;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<h2 className="card-title">Available balance</h2>
|
|
20
|
+
<p className="user-balance flex-col-container flex-grow">
|
|
21
|
+
{balance === undefined && <LoadingSkeleton as="span" className="loading--balance" />}
|
|
22
|
+
{balance !== undefined && (
|
|
23
|
+
<span className="flex-row-container">
|
|
24
|
+
<img src="/eth.svg" alt="" className="balance-icon" />
|
|
25
|
+
<span>{balance}</span>
|
|
26
|
+
<span className="sr-only">Ethereum</span>
|
|
27
|
+
</span>
|
|
28
|
+
)}
|
|
29
|
+
</p>
|
|
30
|
+
<p>
|
|
31
|
+
Get testnet ETH from{" "}
|
|
32
|
+
<a
|
|
33
|
+
href="https://portal.cdp.coinbase.com/products/faucet"
|
|
34
|
+
target="_blank"
|
|
35
|
+
rel="noopener noreferrer"
|
|
36
|
+
>
|
|
37
|
+
Base Sepolia Faucet
|
|
38
|
+
</a>
|
|
39
|
+
</p>
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default UserBalance;
|
|
@@ -1,34 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useSolanaAddress } from "@coinbase/cdp-hooks";
|
|
2
2
|
import { AuthButton } from "@coinbase/cdp-react/components/AuthButton";
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
|
|
5
|
-
import { CDP_CONFIG } from "./config";
|
|
6
5
|
import { IconCheck, IconCopy, IconUser } from "./Icons";
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Header component
|
|
10
9
|
*/
|
|
11
10
|
function Header() {
|
|
12
|
-
const { evmAddress } = useEvmAddress();
|
|
13
11
|
const { solanaAddress } = useSolanaAddress();
|
|
14
|
-
const isSolana = !!CDP_CONFIG.solana;
|
|
15
|
-
const address = isSolana ? solanaAddress : evmAddress;
|
|
16
12
|
const [isCopied, setIsCopied] = useState(false);
|
|
17
13
|
|
|
18
|
-
const formatAddress = useCallback(
|
|
19
|
-
(address
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
? `${address.slice(0, 4)}...${address.slice(-4)}`
|
|
23
|
-
: `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
24
|
-
},
|
|
25
|
-
[isSolana],
|
|
26
|
-
);
|
|
14
|
+
const formatAddress = useCallback((address: string) => {
|
|
15
|
+
if (!address) return "";
|
|
16
|
+
return `${address.slice(0, 4)}...${address.slice(-4)}`;
|
|
17
|
+
}, []);
|
|
27
18
|
|
|
28
19
|
const copyAddress = async () => {
|
|
29
|
-
if (!
|
|
20
|
+
if (!solanaAddress) return;
|
|
30
21
|
try {
|
|
31
|
-
await navigator.clipboard.writeText(
|
|
22
|
+
await navigator.clipboard.writeText(solanaAddress);
|
|
32
23
|
setIsCopied(true);
|
|
33
24
|
} catch (error) {
|
|
34
25
|
console.error(error);
|
|
@@ -43,17 +34,14 @@ function Header() {
|
|
|
43
34
|
return () => clearTimeout(timeout);
|
|
44
35
|
}, [isCopied]);
|
|
45
36
|
|
|
46
|
-
const isSmartAccountsEnabled = import.meta.env.VITE_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE === "smart";
|
|
47
|
-
|
|
48
37
|
return (
|
|
49
38
|
<header>
|
|
50
39
|
<div className="header-inner">
|
|
51
40
|
<div className="title-container">
|
|
52
41
|
<h1 className="site-title">CDP React StarterKit</h1>
|
|
53
|
-
{isSmartAccountsEnabled && <span className="smart-badge">SMART</span>}
|
|
54
42
|
</div>
|
|
55
43
|
<div className="user-info flex-row-container">
|
|
56
|
-
{
|
|
44
|
+
{solanaAddress && (
|
|
57
45
|
<button
|
|
58
46
|
aria-label="copy wallet address"
|
|
59
47
|
className="flex-row-container copy-address-button"
|
|
@@ -66,7 +54,7 @@ function Header() {
|
|
|
66
54
|
</>
|
|
67
55
|
)}
|
|
68
56
|
{isCopied && <IconCheck className="user-icon user-icon--check" />}
|
|
69
|
-
<span className="wallet-address">{formatAddress(
|
|
57
|
+
<span className="wallet-address">{formatAddress(solanaAddress)}</span>
|
|
70
58
|
</button>
|
|
71
59
|
)}
|
|
72
60
|
<AuthButton />
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useIsSignedIn, useSolanaAddress } from "@coinbase/cdp-hooks";
|
|
2
|
+
import { Connection, clusterApiUrl, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import Header from "./Header";
|
|
6
|
+
import SolanaTransaction from "./SolanaTransaction";
|
|
7
|
+
import UserBalance from "./UserBalance";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a Solana connection to access user's balance on Solana Devnet
|
|
11
|
+
*/
|
|
12
|
+
const solanaConnection = new Connection(clusterApiUrl("devnet"));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The Signed In screen
|
|
16
|
+
*/
|
|
17
|
+
function SignedInScreen() {
|
|
18
|
+
const { isSignedIn } = useIsSignedIn();
|
|
19
|
+
const { solanaAddress } = useSolanaAddress();
|
|
20
|
+
const [balance, setBalance] = useState<bigint | undefined>(undefined);
|
|
21
|
+
|
|
22
|
+
const formattedBalance = useMemo(() => {
|
|
23
|
+
if (balance === undefined) return undefined;
|
|
24
|
+
return formatSol(Number(balance));
|
|
25
|
+
}, [balance]);
|
|
26
|
+
|
|
27
|
+
const getBalance = useCallback(async () => {
|
|
28
|
+
if (!solanaAddress) return;
|
|
29
|
+
const lamports = await solanaConnection.getBalance(new PublicKey(solanaAddress));
|
|
30
|
+
setBalance(BigInt(lamports));
|
|
31
|
+
}, [solanaAddress]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
getBalance();
|
|
35
|
+
const interval = setInterval(getBalance, 500);
|
|
36
|
+
return () => clearInterval(interval);
|
|
37
|
+
}, [getBalance]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<Header />
|
|
42
|
+
<main className="main flex-col-container flex-grow">
|
|
43
|
+
<div className="main-inner flex-col-container">
|
|
44
|
+
<div className="card card--user-balance">
|
|
45
|
+
<UserBalance balance={formattedBalance} />
|
|
46
|
+
</div>
|
|
47
|
+
<div className="card card--transaction">
|
|
48
|
+
{isSignedIn && solanaAddress && (
|
|
49
|
+
<SolanaTransaction balance={formattedBalance} onSuccess={getBalance} />
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</main>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format a Solana balance.
|
|
60
|
+
*
|
|
61
|
+
* @param lamports - The balance in lamports.
|
|
62
|
+
* @returns The formatted balance.
|
|
63
|
+
*/
|
|
64
|
+
function formatSol(lamports: number) {
|
|
65
|
+
const maxDecimalPlaces = 9;
|
|
66
|
+
const roundedStr = (lamports / LAMPORTS_PER_SOL).toFixed(maxDecimalPlaces);
|
|
67
|
+
return roundedStr.replace(/0+$/, "").replace(/\.$/, "");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default SignedInScreen;
|