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