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